commit: 8428a1bed38d461e45dddde19758020ab98044e8 parent 2c2de55b054ae909b1454797ada73b7b9b6ec50a Author: Lain Soykaf <lain@lain.com> Date: Wed, 27 Aug 2025 11:43:32 +0400 Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into lazarusDiffstat:
418 files changed, 10700 insertions(+), 2377 deletions(-)diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml@@ -1,8 +1,8 @@ -image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.13.4-otp-25 +image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.14.5-otp-25 variables: &global_variables # Only used for the release - ELIXIR_VER: 1.13.4 + ELIXIR_VER: 1.17.3 POSTGRES_DB: pleroma_test POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -14,9 +14,10 @@ variables: &global_variables workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == "develop" + - if: $CI_COMMIT_BRANCH == "stable" - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - - if: $CI_COMMIT_BRANCH cache: &global_cache_policy key: $CI_JOB_IMAGE-$CI_COMMIT_SHORT_SHA @@ -71,7 +72,7 @@ check-changelog: tags: - amd64 -build-1.13.4-otp-25: +build-1.14.5-otp-25: extends: - .build_changes_policy - .using-ci-base @@ -79,12 +80,12 @@ build-1.13.4-otp-25: script: - mix compile --force -build-1.17.1-otp-26: +build-1.18.3-otp-27: extends: - .build_changes_policy - .using-ci-base stage: build - image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.17.1-otp-26 + image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 script: - mix compile --force @@ -119,7 +120,7 @@ benchmark: - mix ecto.migrate - mix pleroma.load_testing -unit-testing-1.13.4-otp-25: +unit-testing-1.14.5-otp-25: extends: - .build_changes_policy - .using-ci-base @@ -131,10 +132,25 @@ unit-testing-1.13.4-otp-25: - name: postgres:13-alpine alias: postgres command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] + before_script: &testing_before_script + - echo $MIX_ENV + - rm -rf _build/*/lib/pleroma + # Create a non-root user for running tests + - useradd -m -s /bin/bash testuser + # Install dependencies as root first + - mix deps.get + # Set proper ownership for everything + - chown -R testuser:testuser . + - chown -R testuser:testuser /root/.mix || true + - chown -R testuser:testuser /root/.hex || true + # Create user-specific directories + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" script: &testing_script - - mix ecto.create - - mix ecto.migrate - - mix pleroma.test_runner --cover --preload-modules + # Run tests as non-root user + - su testuser -c "HOME=/home/testuser mix ecto.create" + - su testuser -c "HOME=/home/testuser mix ecto.migrate" + - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --cover --preload-modules" coverage: '/^Line total: ([^ ]*%)$/' artifacts: reports: @@ -142,14 +158,15 @@ unit-testing-1.13.4-otp-25: coverage_format: cobertura path: coverage.xml -unit-testing-1.17.1-otp-26: +unit-testing-1.18.3-otp-27: extends: - .build_changes_policy - .using-ci-base stage: test - image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.17.1-otp-26 + image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 cache: *testing_cache_policy services: *testing_services + before_script: *testing_before_script script: *testing_script formatting-1.15: @@ -208,7 +225,7 @@ docs-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline review_app: image: alpine:3.9 stage: deploy @@ -249,7 +266,7 @@ spec-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline stop_review_app: @@ -272,7 +289,8 @@ stop_review_app: amd64: stage: release - image: elixir:$ELIXIR_VER + image: + name: hexpm/elixir-amd64:1.17.3-erlang-26.2.5.6-ubuntu-focal-20241011 only: &release-only - stable@pleroma/pleroma - develop@pleroma/pleroma @@ -297,8 +315,9 @@ amd64: variables: &release-variables MIX_ENV: prod VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive before_script: &before-release - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -313,7 +332,8 @@ amd64-musl: stage: release artifacts: *release-artifacts only: *release-only - image: elixir:$ELIXIR_VER-alpine + image: + name: hexpm/elixir-amd64:1.17.3-erlang-26.2.5.6-alpine-3.17.9 tags: - amd64 cache: *release-cache @@ -327,6 +347,7 @@ amd64-musl: arm: stage: release + allow_failure: true artifacts: *release-artifacts only: *release-only tags: @@ -355,7 +376,8 @@ arm64: only: *release-only tags: - arm - image: arm64v8/elixir:$ELIXIR_VER + image: + name: hexpm/elixir-arm64:1.17.3-erlang-26.2.5.6-ubuntu-focal-20241011 cache: *release-cache variables: *release-variables before_script: *before-release @@ -367,7 +389,8 @@ arm64-musl: only: *release-only tags: - arm - image: arm64v8/elixir:$ELIXIR_VER-alpine + image: + name: hexpm/elixir-arm64:1.17.3-erlang-26.2.5.6-alpine-3.17.9 cache: *release-cache variables: *release-variables before_script: *before-release-musldiff --git a/CHANGELOG.md b/CHANGELOG.md@@ -4,6 +4,121 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.9.1 + +### Security +- Fix authorization checks for C2S Update activities to prevent unauthorized modifications of other users' content. +- Fix content-type spoofing vulnerability that could allow users to upload ActivityPub objects as attachments +- Reject cross-domain redirects when fetching ActivityPub objects to prevent bypassing domain-based security controls. +- Limit emoji shortcodes to alphanumeric, dash, or underscore characters to prevent potential abuse. +- Block attempts to fetch activities from the local instance to prevent spoofing. +- Sanitize Content-Type headers in media proxy to prevent serving malicious ActivityPub content through proxied media. +- Validate Content-Type headers when fetching remote ActivityPub objects to prevent spoofing attacks. + +### Changed +- Include `pl-fe` in available frontends + +### Fixed +- Remove trailing ` from end of line 75 which caused issues copy-pasting + +## 2.9.0 + +### Security +- Require HTTP signatures (if enabled) for routes used by both C2S and S2S AP API +- Fix several spoofing vectors + +### Changed +- Performance: Use 301 (permanent) redirect instead of 302 (temporary) when redirecting small images in media proxy. This allows browsers to cache the redirect response. + +### Added +- Include "published" in actor view +- Link to exported outbox/followers/following collections in backup actor.json +- Hashtag following +- Allow to specify post language + +### Fixed +- Verify a local Update sent through AP C2S so users can only update their own objects +- Fix Mastodon incoming edits with inlined "likes" +- Allow incoming "Listen" activities +- Fix missing check for domain presence in rich media ignore_host configuration +- Fix Rich Media parsing of TwitterCards/OpenGraph to adhere to the spec and always choose the first image if multiple are provided. +- Fix OpenGraph/TwitterCard meta tag ordering for posts with multiple attachments +- Fix blurhash generation crashes + +### Removed +- Retire MRFs DNSRBL, FODirectReply, and QuietReply + +## 2.8.0 + +### Changed +- Metadata: Do not include .atom feed links for remote accounts +- Bumped `fast_html` to v2.3.0, which notably allows to use system-installed lexbor with passing `WITH_SYSTEM_LEXBOR=1` environment variable at build-time +- Dedupe upload filter now uses a three-level sharding directory structure +- Deprecate `/api/v1/pleroma/accounts/:id/subscribe`/`unsubscribe` +- Restrict incoming activities from unknown actors to a subset that does not imply a previous relationship and early rejection of unrecognized activity types. +- Elixir 1.14 and Erlang/OTP 23 is now the minimum supported release +- Support `id` param in `GET /api/v1/statuses` +- LDAP authentication has been refactored to operate as a GenServer process which will maintain an active connection to the LDAP server. +- Fix 'Setting a marker should mark notifications as read' +- Adjust more Oban workers to enforce unique job constraints. +- Oban updated to 2.18.3 +- Publisher behavior improvement when snoozing Oban jobs due to Gun connection pool contention. +- Poll results refreshing is handled asynchronously and will not attempt to keep fetching updates to a closed poll. +- Tuning for release builds to lower CPU usage. +- Rich Media preview fetching will skip making an HTTP HEAD request to check a URL for allowed content type and length if the Tesla adapter is Gun or Finch +- Fix nonexisting user will not generate metadata for search engine opt-out +- Update Oban to 2.18 +- Worker configuration is no longer available. This only affects custom max_retries values for a couple Oban queues. + +### Added +- Add metadata provider for ActivityPub alternate links +- Added support for argon2 passwords and their conversion for migration from Akkoma fork to upstream. +- Respect :restrict_unauthenticated for hashtag rss/atom feeds +- LDAP configuration now permits overriding the CA root certificate file for TLS validation. +- LDAP now supports users changing their passwords +- Include list id in StatusView +- Added MRF.FODirectReply which changes replies to followers-only posts to be direct. +- Add `id_filter` to MRF to filter URLs and their domain prior to fetching +- Added MRF.QuietReply which prevents replies to public posts from being published to the timelines +- Add `group_key` to notifications +- Allow providing avatar/header descriptions +- Added RemoteReportPolicy from Rebased for handling bogus federated reports +- scrubbers/default: Allow "mention hashtag" classes used by Mastodon +- Added dependencies for Swoosh's Mua mail adapter +- Include session scopes in TokenView + +### Fixed +- Verify a local Update sent through AP C2S so users can only update their own objects +- Fixed malformed follow requests that cause them to appear stuck pending due to the recipient being unable to process them. +- Fix incoming Block activities being rejected +- STARTTLS certificate and hostname verification for LDAP authentication +- LDAPS connections (implicit TLS) are now supported. +- Fix /api/v2/media returning the wrong status code (202) for media processed synchronously +- Miscellaneous fixes for Meilisearch support +- Fix pleroma_ctl mix task calls sometimes not being found +- Add a rate limiter to the OAuth App creation endpoint and ensure registered apps are assigned to users. +- ReceiverWorker will cancel processing jobs instead of retrying if the user cannot be fetched due to 403, 404, or 410 errors or if the account is disabled locally. +- Address case where instance reachability status couldn't be updated +- Remote Fetcher Worker recognizes more permanent failure errors +- StreamerView: Do not leak follows count if hidden +- Imports of blocks, mutes, and follows would retry repeatedly due to incorrect error handling and all work executed in a single job +- Make vapid_config return empty array, fixing preloading for instances without push notifications configured + +### Removed +- Remove stub for /api/v1/accounts/:id/identity_proofs (deprecated by Mastodon 3.5.0) + +## 2.7.1 + +### Changed +- Accept `application/activity+json` for requests to `/.well-known/nodeinfo` + +### Fixed +- Truncate remote user fields, avoids them getting rejected +- Improve the `FollowValidator` to successfully incoming activities with an errant `cc` field. +- Resolved edge case where the API can report you are following a user but the relationship is not fully established. +- The Swoosh email adapter for Mailgun was missing a new dependency on `:multipart` +- Fix Mastodon WebSocket authentication + ## 2.7.0 ### Securitydiff --git a/Dockerfile b/Dockerfile@@ -1,7 +1,8 @@ +# https://hub.docker.com/r/hexpm/elixir/tags ARG ELIXIR_IMG=hexpm/elixir -ARG ELIXIR_VER=1.13.4 -ARG ERLANG_VER=24.3.4.15 -ARG ALPINE_VER=3.17.5 +ARG ELIXIR_VER=1.14.5 +ARG ERLANG_VER=25.3.2.14 +ARG ALPINE_VER=3.17.9 FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} as builddiff --git a/changelog.d/activity_type_index.change b/changelog.d/activity_type_index.change@@ -0,0 +1 @@ +Add new activity actor/type index. Greatly speeds up retrieval of rare types (like "Listen")diff --git a/changelog.d/admin-api-docs-fix.skip b/changelog.d/admin-api-docs-fix.skip@@ -0,0 +1 @@ +Fix 'Create a user' description in admin api docsdiff --git a/changelog.d/ci-git-fetch.skip b/changelog.d/admin-api-log-fix.skipdiff --git a/changelog.d/argon2-passwords.add b/changelog.d/argon2-passwords.add@@ -1 +0,0 @@ -Added support for argon2 passwords and their conversion for migration from Akkoma fork to upstream.diff --git a/changelog.d/assign-app-user-oom.fix b/changelog.d/assign-app-user-oom.fix@@ -0,0 +1 @@ +Fix AssignAppUser migration OOMdiff --git a/changelog.d/bugfix-truncate-remote-user-fields.fix b/changelog.d/bugfix-truncate-remote-user-fields.fix@@ -1 +0,0 @@ -Truncate remote user fields, avoids them getting rejecteddiff --git a/changelog.d/bump-captcha-posix-make.fix b/changelog.d/bump-captcha-posix-make.fix@@ -0,0 +1 @@ +- Fix building "captcha" library with OpenBSD make +\ No newline at end of filediff --git a/changelog.d/db-restore-docs.change b/changelog.d/db-restore-docs.change@@ -0,0 +1 @@ +Docs: Restore DB schema before data to avoid long restore timesdiff --git a/changelog.d/deactivated-404-inbox.change b/changelog.d/deactivated-404-inbox.change@@ -0,0 +1 @@ +Return 404 with a better error message instead of 400 when receiving an activity for a deactivated user +\ No newline at end of filediff --git a/changelog.d/deepl-json.fix b/changelog.d/deepl-json.fix@@ -0,0 +1 @@ +Use JSON for DeepL API requestsdiff --git a/changelog.d/delete-instance.change b/changelog.d/delete-instance.change@@ -0,0 +1 @@ +Deleting an instance queues individual jobs for each user that needs to be deleted from the server.diff --git a/changelog.d/deprecate-subscribe.change b/changelog.d/deprecate-subscribe.change@@ -1 +0,0 @@ -Deprecate `/api/v1/pleroma/accounts/:id/subscribe`/`unsubscribe` -\ No newline at end of filediff --git a/changelog.d/commonapi.skip b/changelog.d/deps-update-2025-08.skipdiff --git a/changelog.d/dislike-activity.add b/changelog.d/dislike-activity.add@@ -0,0 +1 @@ +Support Dislike activity, as sent by Mitra and Friendica, by changing it into a thumbs-down EmojiReact +\ No newline at end of filediff --git a/changelog.d/dialyzer.skip b/changelog.d/doc-typo.skipdiff --git a/changelog.d/drop-unwanted.change b/changelog.d/drop-unwanted.change@@ -1 +0,0 @@ -Restrict incoming activities from unknown actors to a subset that does not imply a previous relationship and early rejection of unrecognized activity types.diff --git a/changelog.d/elixir-1-18.fix b/changelog.d/elixir-1-18.fix@@ -0,0 +1 @@ +Elixir 1.18: Fixed warnings and new deprecationsdiff --git a/changelog.d/emoji-pack-upload-zip.add b/changelog.d/emoji-pack-upload-zip.add@@ -0,0 +1 @@ +Added a way to upload new packs from a URL or ZIP file via Admin API +\ No newline at end of filediff --git a/changelog.d/emoji_likes.add b/changelog.d/emoji_likes.add@@ -0,0 +1 @@ +Support Mitra-style emoji likes.diff --git a/changelog.d/endorsement-state.fix b/changelog.d/endorsement-state.fix@@ -0,0 +1 @@ +Fix endorsement state display in relationship viewdiff --git a/changelog.d/expiring-blocks.add b/changelog.d/expiring-blocks.add@@ -0,0 +1 @@ +Add `duration` to the block endpoint, which makes block expire +\ No newline at end of filediff --git a/changelog.d/expose-markup-configuration.add b/changelog.d/expose-markup-configuration.add@@ -0,0 +1 @@ +Expose markup configuration in InstanceViewdiff --git a/changelog.d/docs-fix.skip b/changelog.d/fixtests.skipdiff --git a/changelog.d/follow-request.fix b/changelog.d/follow-request.fix@@ -1 +0,0 @@ -Fixed malformed follow requests that cause them to appear stuck pending due to the recipient being unable to process them.diff --git a/changelog.d/follow-validator.fix b/changelog.d/follow-validator.fix@@ -1 +0,0 @@ -Improve the FollowValidator to successfully incoming activities with an errant cc field.diff --git a/changelog.d/following-state.fix b/changelog.d/following-state.fix@@ -1 +0,0 @@ -Resolved edge case where the API can report you are following a user but the relationship is not fully established.diff --git a/changelog.d/freebsd-rc.fix b/changelog.d/freebsd-rc.fix@@ -0,0 +1 @@ +Set PATH in the FreeBSD rc script to avoid failures starting the servicediff --git a/changelog.d/get-statuses-param.change b/changelog.d/get-statuses-param.change@@ -1 +0,0 @@ -Support `id` param in `GET /api/v1/statuses` -\ No newline at end of filediff --git a/changelog.d/gin-search.fix b/changelog.d/gin-search.fix@@ -0,0 +1 @@ +Improved performance of status search queries using the default GIN indexdiff --git a/changelog.d/manifest-icon-size.skip b/changelog.d/gitlabci.skipdiff --git a/changelog.d/gun.change b/changelog.d/gun.change@@ -0,0 +1 @@ +Update Cowboy, Gun, and Plug family of dependenciesdiff --git a/changelog.d/hashtag-search.change b/changelog.d/hashtag-search.change@@ -0,0 +1 @@ +Hashtag searches return real results based on words in your querydiff --git a/changelog.d/identity-proofs.remove b/changelog.d/identity-proofs.remove@@ -1 +0,0 @@ -Remove stub for /api/v1/accounts/:id/identity_proofs (deprecated by Mastodon 3.5.0) -\ No newline at end of filediff --git a/changelog.d/language-detection.add b/changelog.d/language-detection.add@@ -0,0 +1 @@ +Implement language detection with fastText +\ No newline at end of filediff --git a/changelog.d/ldap-ca.add b/changelog.d/ldap-ca.add@@ -1 +0,0 @@ -LDAP configuration now permits overriding the CA root certificate file for TLS validation.diff --git a/changelog.d/ldap-refactor.change b/changelog.d/ldap-refactor.change@@ -1 +0,0 @@ -LDAP authentication has been refactored to operate as a GenServer process which will maintain an active connection to the LDAP server.diff --git a/changelog.d/ldap-tls.fix b/changelog.d/ldap-tls.fix@@ -1 +0,0 @@ -STARTTLS certificate and hostname verification for LDAP authenticationdiff --git a/changelog.d/ldaps.fix b/changelog.d/ldaps.fix@@ -1 +0,0 @@ -LDAPS connections (implicit TLS) are now supported.diff --git a/changelog.d/list-id-visibility.add b/changelog.d/list-id-visibility.add@@ -1 +0,0 @@ -Include list id in StatusView -\ No newline at end of filediff --git a/changelog.d/mailgun.fix b/changelog.d/mailgun.fix@@ -1 +0,0 @@ -The Swoosh email adapter for Mailgun was missing a new dependency on :multipartdiff --git a/changelog.d/mrf-fodirectreply.add b/changelog.d/mrf-fodirectreply.add@@ -1 +0,0 @@ -Added MRF.FODirectReply which changes replies to followers-only posts to be direct.diff --git a/changelog.d/mogrify.skip b/changelog.d/noop-fixes.skipdiff --git a/changelog.d/notifications-group-key.add b/changelog.d/notifications-group-key.add@@ -1 +0,0 @@ -Add `group_key` to notifications -\ No newline at end of filediff --git a/changelog.d/notifications-marker.change b/changelog.d/notifications-marker.change@@ -1 +0,0 @@ -Fix 'Setting a marker should mark notifications as read' -\ No newline at end of filediff --git a/changelog.d/oauth-app-spam.fix b/changelog.d/oauth-app-spam.fix@@ -1 +0,0 @@ -Add a rate limiter to the OAuth App creation endpoint and ensure registered apps are assigned to users.diff --git a/changelog.d/oban-notifier.change b/changelog.d/oban-notifier.change@@ -0,0 +1 @@ +Oban Notifier was changed to Oban.Notifiers.PG for performance and scalability benefitsdiff --git a/changelog.d/oban-recevier-improvements.fix b/changelog.d/oban-recevier-improvements.fix@@ -1 +0,0 @@ -ReceiverWorker will cancel processing jobs instead of retrying if the user cannot be fetched due to 403, 404, or 410 errors or if the account is disabled locally.diff --git a/changelog.d/oban-uniques.change b/changelog.d/oban-uniques.change@@ -1 +0,0 @@ -Adjust more Oban workers to enforce unique job constraints.diff --git a/changelog.d/oban_gun_snooze.change b/changelog.d/oban_gun_snooze.change@@ -1 +0,0 @@ -Publisher behavior improvement when snoozing Oban jobs due to Gun connection pool contention.diff --git a/changelog.d/mrf-cleanup.skip b/changelog.d/openbsd-docs-update.skipdiff --git a/changelog.d/openbsd-update-httpd-relayd.change b/changelog.d/openbsd-update-httpd-relayd.change@@ -0,0 +1 @@ +Updated relayd/httpd config files to be on par with nginxdiff --git a/changelog.d/openbsd-update-rc.fix b/changelog.d/openbsd-update-rc.fix@@ -0,0 +1 @@ +replaced depracated flags and functions, renamed service to fit other service filesdiff --git a/changelog.d/postgrex.change b/changelog.d/postgrex.change@@ -0,0 +1 @@ +Updated Postgrex library to 0.20.0diff --git a/changelog.d/preserve-public-cc.fix b/changelog.d/preserve-public-cc.fix@@ -0,0 +1 @@ +Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instancesdiff --git a/changelog.d/text-extensions.skip b/changelog.d/private-functions.skipdiff --git a/changelog.d/profile-image-descriptions.add b/changelog.d/profile-image-descriptions.add@@ -1 +0,0 @@ -Allow providing avatar/header descriptions -\ No newline at end of filediff --git a/changelog.d/publisher-reachability.fix b/changelog.d/publisher-reachability.fix@@ -1 +0,0 @@ -Address case where instance reachability status couldn't be updateddiff --git a/changelog.d/reachability.change b/changelog.d/reachability.change@@ -0,0 +1 @@ +Improved the logic of how we determine if a server is unreachable.diff --git a/changelog.d/relax-also-known-as.change b/changelog.d/relax-also-known-as.change@@ -0,0 +1 @@ +Relax alsoKnownAs requirements to just URI, not necessarily HTTP(S) +\ No newline at end of filediff --git a/changelog.d/releases.fix b/changelog.d/releases.fix@@ -0,0 +1 @@ +Fix release buildsdiff --git a/changelog.d/remote-object-fetcher.fix b/changelog.d/remote-object-fetcher.fix@@ -1 +0,0 @@ -Remote Fetcher Worker recognizes more permanent failure errorsdiff --git a/changelog.d/todo-cleanup.skip b/changelog.d/remove-forgotten-OTPVersion-usage.skipdiff --git a/changelog.d/rich-media-no-heads.change b/changelog.d/rich-media-no-heads.change@@ -1 +0,0 @@ -Rich Media preview fetching will skip making an HTTP HEAD request to check a URL for allowed content type and length if the Tesla adapter is Gun or Finchdiff --git a/changelog.d/scrobbles.change b/changelog.d/scrobbles.change@@ -0,0 +1 @@ +Change scrobble external link param name to use snake case +\ No newline at end of filediff --git a/changelog.d/scrubbers-allow-mention-hashtag.add b/changelog.d/scrubbers-allow-mention-hashtag.add@@ -1 +0,0 @@ -scrubbers/default: Allow "mention hashtag" classes used by Mastodon -\ No newline at end of filediff --git a/changelog.d/siteinfo-baseurls.add b/changelog.d/siteinfo-baseurls.add@@ -0,0 +1 @@ +Add `base_urls` to the /api/v1/instance pleroma metadata which provides information about the base URLs for media_proxy and uploads when configured +\ No newline at end of filediff --git a/changelog.d/smtp-docs.change b/changelog.d/smtp-docs.change@@ -0,0 +1 @@ +Change SMTP example to use the Mua adapter that works with OTP>25 +\ No newline at end of filediff --git a/changelog.d/stream-follow-relationships-count.fix b/changelog.d/stream-follow-relationships-count.fix@@ -1 +0,0 @@ -StreamerView: Do not leak follows count if hidden -\ No newline at end of filediff --git a/changelog.d/tesla.change b/changelog.d/tesla.change@@ -0,0 +1 @@ +Updated Tesla to 1.15.3diff --git a/changelog.d/toctou-mkdir.fix b/changelog.d/toctou-mkdir.fix@@ -0,0 +1 @@ +Backport [Elixir PR 14242](https://github.com/elixir-lang/elixir/pull/14242) fixing racy mkdir and lack of error handling of parent directory creation +\ No newline at end of filediff --git a/changelog.d/tos-setting.add b/changelog.d/tos-setting.add@@ -0,0 +1 @@ +Allow Terms of Service panel behaviour to be configurablediff --git a/changelog.d/translate-posts.add b/changelog.d/translate-posts.add@@ -0,0 +1 @@ +Support translation providers (DeepL, LibreTranslate) +\ No newline at end of filediff --git a/changelog.d/truncate-rich-media.change b/changelog.d/truncate-rich-media.change@@ -0,0 +1 @@ +Truncate the length of Rich Media title and description fieldsdiff --git a/changelog.d/user-factory.skip b/changelog.d/typos.skipdiff --git a/changelog.d/update-oban.change b/changelog.d/update-oban.change@@ -1 +0,0 @@ -Update Oban to 2.18diff --git a/changelog.d/url-encoding.fix b/changelog.d/url-encoding.fix@@ -0,0 +1 @@ +Fix HTTP client making invalid requests due to no percent encoding processing or validation.diff --git a/changelog.d/user-imports.fix b/changelog.d/user-imports.fix@@ -1 +0,0 @@ -Imports of blocks, mutes, and follows would retry repeatedly due to incorrect error handling and all work executed in a single jobdiff --git a/changelog.d/webfinger-resolution.fix b/changelog.d/webfinger-resolution.fix@@ -0,0 +1 @@ +Enforce an exact domain match for WebFinger resolutiondiff --git a/changelog.d/webfinger.change b/changelog.d/webfinger.change@@ -0,0 +1 @@ +Don't require an Accept header for WebFinger queries and default to JSON. +\ No newline at end of filediff --git a/changelog.d/well-known.change b/changelog.d/well-known.change@@ -1 +0,0 @@ -Accept application/activity+json for requests to .well-known/nodeinfodiff --git a/changelog.d/workerhelper.change b/changelog.d/workerhelper.change@@ -1 +0,0 @@ -Worker configuration is no longer available. This only affects custom max_retries values for a couple Oban queues.diff --git a/ci/elixir-1.12/Dockerfile b/ci/elixir-1.12/Dockerfile@@ -1,8 +0,0 @@ -FROM elixir:1.12.3 - -# Single RUN statement, otherwise intermediate images are created -# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run -RUN apt-get update &&\ - apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\ - mix local.hex --force &&\ - mix local.rebar --forcediff --git a/ci/elixir-1.12/build_and_push.sh b/ci/elixir-1.12/build_and_push.sh@@ -1 +0,0 @@ -docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.12 --push .diff --git a/ci/elixir-1.13.4-otp-25/Dockerfile b/ci/elixir-1.13.4-otp-25/Dockerfile@@ -1,8 +0,0 @@ -FROM elixir:1.13.4-otp-25 - -# Single RUN statement, otherwise intermediate images are created -# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run -RUN apt-get update &&\ - apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\ - mix local.hex --force &&\ - mix local.rebar --forcediff --git a/ci/elixir-1.13.4-otp-25/build_and_push.sh b/ci/elixir-1.13.4-otp-25/build_and_push.sh@@ -1 +0,0 @@ -docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.13.4-otp-25 --push .diff --git a/ci/elixir-1.14.5-otp-25/Dockerfile b/ci/elixir-1.14.5-otp-25/Dockerfile@@ -0,0 +1,8 @@ +FROM elixir:1.14.5-otp-25 + +# Single RUN statement, otherwise intermediate images are created +# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run +RUN apt-get update &&\ + apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\ + mix local.hex --force &&\ + mix local.rebar --forcediff --git a/ci/elixir-1.14.5-otp-25/build_and_push.sh b/ci/elixir-1.14.5-otp-25/build_and_push.sh@@ -0,0 +1 @@ +docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.14.5-otp-25 --push .diff --git a/ci/elixir-1.18.3-otp-27/Dockerfile b/ci/elixir-1.18.3-otp-27/Dockerfile@@ -0,0 +1,8 @@ +FROM elixir:1.18.3-otp-27 + +# Single RUN statement, otherwise intermediate images are created +# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run +RUN apt-get update &&\ + apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\ + mix local.hex --force &&\ + mix local.rebar --forcediff --git a/ci/elixir-1.18.3-otp-27/build_and_push.sh b/ci/elixir-1.18.3-otp-27/build_and_push.sh@@ -0,0 +1 @@ +docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 --push .diff --git a/config/config.exs b/config/config.exs@@ -48,7 +48,7 @@ config :pleroma, ecto_repos: [Pleroma.Repo] config :pleroma, Pleroma.Repo, telemetry_event: [Pleroma.Repo.Instrumenter], - migration_lock: nil + migration_lock: :pg_advisory_lock config :pleroma, Pleroma.Captcha, enabled: true, @@ -65,7 +65,8 @@ config :pleroma, Pleroma.Upload, proxy_remote: false, filename_display_max_length: 30, default_description: nil, - base_url: nil + base_url: nil, + allowed_mime_types: ["image", "audio", "video"] config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" @@ -150,7 +151,10 @@ config :mime, :types, %{ "application/xrd+xml" => ["xrd+xml"], "application/jrd+json" => ["jrd+json"], "application/activity+json" => ["activity+json"], - "application/ld+json" => ["activity+json"] + "application/ld+json" => ["activity+json"], + # Can be removed when bumping MIME past 2.0.5 + # see https://akkoma.dev/AkkomaGang/akkoma/issues/657 + "image/apng" => ["apng"] } config :tesla, adapter: Tesla.Adapter.Hackney @@ -190,7 +194,6 @@ config :pleroma, :instance, account_approval_required: false, federating: true, federation_incoming_replies_max_depth: 100, - federation_reachability_timeout_days: 7, allow_relay: true, public: true, quarantined_instances: [], @@ -303,6 +306,7 @@ config :pleroma, :frontend_configurations, collapseMessageWithSubject: false, disableChat: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, @@ -359,7 +363,8 @@ config :pleroma, :activitypub, follow_handshake_timeout: 500, note_replies_output_limit: 5, sign_object_fetches: true, - authorized_fetch_mode: false + authorized_fetch_mode: false, + client_api_enabled: false config :pleroma, :streamer, workers: 3, @@ -413,11 +418,6 @@ config :pleroma, :mrf_vocabulary, accept: [], reject: [] -config :pleroma, :mrf_dnsrbl, - nameserver: "127.0.0.1", - port: 53, - zone: "bl.pleroma.com" - # threshold of 7 days config :pleroma, :mrf_object_age, threshold: 604_800, @@ -434,6 +434,11 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}" +config :pleroma, :mrf_remote_report, + reject_all: false, + reject_anonymous: true, + reject_empty_message: true + config :pleroma, :mrf_force_mention, mention_parent: true, mention_quoted: true @@ -584,6 +589,7 @@ config :pleroma, Pleroma.User, # value or it cannot enforce uniqueness. config :pleroma, Oban, repo: Pleroma.Repo, + notifier: Oban.Notifiers.PG, log: false, queues: [ activity_expiration: 10, @@ -802,6 +808,13 @@ config :pleroma, :frontends, "https://lily-is.land/infra/glitch-lily/-/jobs/artifacts/${ref}/download?job=build", "ref" => "servant", "build_dir" => "public" + }, + "pl-fe" => %{ + "name" => "pl-fe", + "git" => "https://github.com/mkljczk/pl-fe", + "build_url" => "https://pl.mkljczk.pl/pl-fe.zip", + "ref" => "develop", + "build_dir" => "." } }diff --git a/config/description.exs b/config/description.exs@@ -117,6 +117,19 @@ config :pleroma, :config_description, [ key: :filename_display_max_length, type: :integer, description: "Set max length of a filename to display. 0 = no limit. Default: 30" + }, + %{ + key: :allowed_mime_types, + label: "Allowed MIME types", + type: {:list, :string}, + description: + "List of MIME (main) types uploads are allowed to identify themselves with. Other types may still be uploaded, but will identify as a generic binary to clients. WARNING: Loosening this over the defaults can lead to security issues. Removing types is safe, but only add to the list if you are sure you know what you are doing.", + suggestions: [ + "image", + "audio", + "video", + "font" + ] } ] }, @@ -1248,6 +1261,7 @@ config :pleroma, :config_description, [ background: "/static/aurora_borealis.jpg", collapseMessageWithSubject: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, @@ -1300,6 +1314,12 @@ config :pleroma, :config_description, [ description: "Enables green text on lines prefixed with the > character" }, %{ + key: :embeddedToS, + label: "Embedded ToS panel", + type: :boolean, + description: "Hide Terms of Service panel decorations on About and Registration pages" + }, + %{ key: :hideFilteredStatuses, label: "Hide Filtered Statuses", type: :boolean, @@ -1772,6 +1792,11 @@ config :pleroma, :config_description, [ type: :integer, description: "Following handshake timeout", suggestions: [500] + }, + %{ + key: :client_api_enabled, + type: :boolean, + description: "Allow client to server ActivityPub interactions" } ] }, @@ -3302,8 +3327,7 @@ config :pleroma, :config_description, [ suggestions: [ Pleroma.Web.Preload.Providers.Instance, Pleroma.Web.Preload.Providers.User, - Pleroma.Web.Preload.Providers.Timelines, - Pleroma.Web.Preload.Providers.StatusNet + Pleroma.Web.Preload.Providers.Timelines ] } ] @@ -3483,5 +3507,71 @@ config :pleroma, :config_description, [ suggestion: [100_000] } ] + }, + %{ + group: :pleroma, + key: Pleroma.Language.LanguageDetector, + type: :group, + description: "Language detection providers", + children: [ + %{ + key: :provider, + type: :module, + suggestions: [ + Pleroma.Language.LanguageDetector.Fasttext + ] + }, + %{ + group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext}, + key: :model, + label: "fastText language detection model", + type: :string, + suggestions: ["/usr/share/fasttext/lid.176.bin"] + } + ] + }, + %{ + group: :pleroma, + key: Pleroma.Language.Translation, + type: :group, + description: "Translation providers", + children: [ + %{ + key: :provider, + type: :module, + suggestions: [ + Pleroma.Language.Translation.Deepl, + Pleroma.Language.Translation.Libretranslate + ] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Deepl}, + key: :base_url, + label: "DeepL base URL", + type: :string, + suggestions: ["https://api-free.deepl.com", "https://api.deepl.com"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Deepl}, + key: :api_key, + label: "DeepL API Key", + type: :string, + suggestions: ["YOUR_API_KEY"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Libretranslate}, + key: :base_url, + label: "LibreTranslate instance URL", + type: :string, + suggestions: ["https://libretranslate.com"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Libretranslate}, + key: :api_key, + label: "LibreTranslate API Key", + type: :string, + suggestions: ["YOUR_API_KEY"] + } + ] } ]diff --git a/config/test.exs b/config/test.exs@@ -38,7 +38,10 @@ config :pleroma, :instance, external_user_synchronization: false, static_dir: "test/instance_static/" -config :pleroma, :activitypub, sign_object_fetches: false, follow_handshake_timeout: 0 +config :pleroma, :activitypub, + sign_object_fetches: false, + follow_handshake_timeout: 0, + client_api_enabled: true # Configure your database config :pleroma, Pleroma.Repo, @@ -144,6 +147,7 @@ config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", priv config :phoenix, :plug_init_mode, :runtime config :pleroma, :config_impl, Pleroma.UnstubbedConfigMock +config :pleroma, :datetime_impl, Pleroma.DateTimeMock config :pleroma, Pleroma.PromEx, disabled: true @@ -152,12 +156,19 @@ config :pleroma, Pleroma.User.Backup, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.Language.LanguageDetector, config_impl: Pleroma.StaticStubbedConfigMock config :pleroma, Pleroma.ScheduledActivity, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock config :pleroma, Pleroma.Uploaders.IPFS, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Web.Plugs.HTTPSecurityPlug, config_impl: Pleroma.StaticStubbedConfigMock config :pleroma, Pleroma.Web.Plugs.HTTPSignaturePlug, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Upload.Filter.AnonymizeFilename, + config_impl: Pleroma.StaticStubbedConfigMock + +config :pleroma, Pleroma.Upload.Filter.Mogrify, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Upload.Filter.Mogrify, mogrify_impl: Pleroma.MogrifyMock + config :pleroma, Pleroma.Signature, http_signatures_impl: Pleroma.StubbedHTTPSignaturesMock peer_module =diff --git a/docs/administration/backup.md b/docs/administration/backup.md@@ -2,28 +2,60 @@ ## Backup -1. Stop the Pleroma service. -2. Go to the working directory of Pleroma (default is `/opt/pleroma`) -3. Run `sudo -Hu postgres pg_dump -d <pleroma_db> --format=custom -f </path/to/backup_location/pleroma.pgdump>` (make sure the postgres user has write access to the destination file) +1. Stop the Pleroma service: +``` +# sudo systemctl stop pleroma +``` +2. Go to the working directory of Pleroma (default is `/opt/pleroma`). +3. Run (make sure the postgres user has write access to the destination file): +``` +# sudo -Hu postgres pg_dump -d <pleroma_db> -v --format=custom --compress=9 -f </path/to/backup_location/pleroma.pgdump> +``` 4. Copy `pleroma.pgdump`, `config/prod.secret.exs`, `config/setup_db.psql` (if still available) and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too. -5. Restart the Pleroma service. +5. Restart the Pleroma service: +``` +# sudo systemctl start pleroma +``` ## Restore/Move 1. Optionally reinstall Pleroma (either on the same server or on another server if you want to move servers). -2. Stop the Pleroma service. -3. Go to the working directory of Pleroma (default is `/opt/pleroma`) +2. Stop the Pleroma service: +``` +# sudo systemctl stop pleroma +``` +3. Go to the working directory of Pleroma (default is `/opt/pleroma`). 4. Copy the above mentioned files back to their original position. -5. Drop the existing database and user if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE <pleroma_db>;';` `sudo -Hu postgres psql -c 'DROP USER <pleroma_db>;'` -6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. +5. Drop the existing database and user if restoring in-place: +``` +# sudo -Hu postgres dropdb <pleroma_db> +# sudo -Hu postgres dropuser <pleroma_user> +``` +6. Restore the database schema and pleroma database user the with the original `setup_db.psql` if you have it: +``` +# sudo -Hu postgres psql -f config/setup_db.psql +``` - Alternatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. + Alternatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma user and schema with the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. -7. Now restore the Pleroma instance's data into the empty database schema: `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>` -8. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. -9. Restart the Pleroma service. -10. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries. -11. If setting up on a new server configure Nginx by using the `installation/pleroma.nginx` config sample or reference the Pleroma installation guide for your OS which contains the Nginx configuration instructions. +7. Now restore the Pleroma instance's schema into the empty database schema: +``` +# sudo -Hu postgres pg_restore -d <pleroma_db> -v -s -1 </path/to/backup_location/pleroma.pgdump> +``` +8. Now restore the Pleroma instance's data into the database: +``` +# sudo -Hu postgres pg_restore -d <pleroma_db> -v -a -1 --disable-triggers </path/to/backup_location/pleroma.pgdump> +``` +9. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. +10. Generate the statistics so that PostgreSQL can properly plan queries: +``` +# sudo -Hu postgres vacuumdb -v --all --analyze-in-stages +``` +11. Restart the Pleroma service: +``` +# sudo systemctl start pleroma +``` +12. If setting up on a new server, configure Nginx by using your original configuration or by using the `installation/pleroma.nginx` config sample or reference the Pleroma installation guide for your OS which contains the Nginx configuration instructions. [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. @@ -32,10 +64,26 @@ 1. Optionally you can remove the users of your instance. This will trigger delete requests for their accounts and posts. Note that this is 'best effort' and doesn't mean that all traces of your instance will be gone from the fediverse. * You can do this from the admin-FE where you can select all local users and delete the accounts using the *Moderate multiple users* dropdown. * You can also list local users and delete them individually using the CLI tasks for [Managing users](./CLI_tasks/user.md). -2. Stop the Pleroma service `systemctl stop pleroma` -3. Disable pleroma from systemd `systemctl disable pleroma` +2. Stop the Pleroma service: +``` +# systemctl stop pleroma +``` +3. Disable pleroma from systemd: +``` +# systemctl disable pleroma +``` 4. Remove the files and folders you created during installation (see installation guide). This includes the pleroma, nginx and systemd files and folders. -5. Reload nginx now that the configuration is removed `systemctl reload nginx` -6. Remove the database and database user `sudo -Hu postgres psql -c 'DROP DATABASE <pleroma_db>;';` `sudo -Hu postgres psql -c 'DROP USER <pleroma_db>;'` -7. Remove the system user `userdel pleroma` -8. Remove the dependencies that you don't need anymore (see installation guide). Make sure you don't remove packages that are still needed for other software that you have running! +5. Reload nginx now that the configuration is removed: +``` +# systemctl reload nginx +``` +6. Remove the database and database user: +``` +# sudo -Hu postgres dropdb <pleroma_db> +# sudo -Hu postgres dropuser <pleroma_user> +``` +7. Remove the system user: +``` +# userdel -r pleroma +``` +8. Remove the dependencies that you don't need anymore (see installation guide). **Make sure you don't remove packages that are still needed for other software that you have running!**diff --git a/docs/clients.md b/docs/clients.md@@ -28,6 +28,7 @@ Feel free to contact us to be added to this list! ### AndStatus - Homepage: <http://andstatus.org/> - Source Code: <https://github.com/andstatus/andstatus/> +- Contact: [@AndStatus@mastodon.social](https://mastodon.social/@AndStatus) - Platforms: Android - Features: MastoAPI, ActivityPub (Client-to-Server) @@ -40,8 +41,8 @@ Feel free to contact us to be added to this list! ### Fedilab - Homepage: <https://fedilab.app/> -- Source Code: <https://framagit.org/tom79/fedilab/> -- Contact: [@fedilab@framapiaf.org](https://framapiaf.org/users/fedilab) +- Source Code: <https://codeberg.org/tom79/Fedilab> +- Contact: [@apps@toot.fedilab.app](https://toot.fedilab.app/@apps) - Platforms: Android - Features: MastoAPI, Streaming Ready, Moderation, Text Formatting @@ -51,8 +52,8 @@ Feel free to contact us to be added to this list! - Features: MastoAPI, No Streaming ### Husky -- Source code: <https://git.mentality.rip/FWGS/Husky> -- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky) +- Source code: <https://github.com/captainepoch/husky> +- Contact: [@husky@stereophonic.space](https://stereophonic.space/users/husky) - Platforms: Android - Features: MastoAPI, No Streaming, Emoji Reactions, Text Formatting, FE Stickers @@ -65,7 +66,7 @@ Feel free to contact us to be added to this list! ### Tusky - Homepage: <https://tuskyapp.github.io/> - Source Code: <https://github.com/tuskyapp/Tusky> -- Contact: [@ConnyDuck@mastodon.social](https://mastodon.social/users/ConnyDuck) +- Contact: [@Tusky@mastodon.social](https://mastodon.social/@Tusky) - Platforms: Android - Features: MastoAPI, No Streaming @@ -76,10 +77,10 @@ Feel free to contact us to be added to this list! - Platform: Android - Features: MastoAPI, No Streaming -### Indigenous -- Homepage: <https://indigenous.realize.be/> -- Source Code: <https://github.com/swentel/indigenous-android/> -- Contact: [@swentel@realize.be](https://realize.be) +### IndiePass +- Homepage: <https://indiepass.app/> +- Source Code: <https://github.com/IndiePass/indiepass-android> +- Contact: [@marksuth@mastodon.social](https://mastodon.social/@marksuth) - Platforms: Android - Features: MastoAPI, No Streamingdiff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md@@ -98,7 +98,7 @@ To add configuration to your config file, you can copy it from the base config. * `moderator_privileges`: A list of privileges a moderator has (e.g. delete messages, manage reports...) * Possible values are the same as for `admin_privileges` -## :database +## :features * `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes). ## Background migrations @@ -733,13 +733,26 @@ An example for SMTP adapter: ```elixir config :pleroma, Pleroma.Emails.Mailer, enabled: true, - adapter: Swoosh.Adapters.SMTP, + adapter: Swoosh.Adapters.Mua, relay: "smtp.gmail.com", - username: "YOUR_USERNAME@gmail.com", - password: "YOUR_SMTP_PASSWORD", + auth: [username: "YOUR_USERNAME@gmail.com", password: "YOUR_SMTP_PASSWORD"], port: 465, - ssl: true, - auth: :always + protocol: :ssl +``` + +An example for Mua adapter: + +```elixir +config :pleroma, Pleroma.Emails.Mailer, + enabled: true, + adapter: Swoosh.Adapters.Mua, + relay: "mail.example.com", + port: 465, + auth: [ + username: "YOUR_USERNAME@domain.tld", + password: "YOUR_SMTP_PASSWORD" + ], + protocol: :ssl ``` ### :email_notificationsdiff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md@@ -70,6 +70,8 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad - `nicknames` - Response: Array of user nicknames +## `POST /api/v1/pleroma/admin/users` + ### Create a user - Method: `POST` @@ -81,7 +83,7 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad `password` } ] -- Response: User’s nickname +- Response: Array of user objects ## `POST /api/v1/pleroma/admin/users/follow`diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md@@ -671,6 +671,7 @@ Audio scrobbling in Pleroma is **deprecated**. "artist": "Some Artist", "album": "Some Album", "length": 180000, + "external_link": "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at": "2019-09-28T12:40:45.000Z" } ]diff --git a/docs/development/index.md b/docs/development/index.md@@ -1 +1,7 @@ This section contains notes and guidelines for developers. + +- [Setting up a Pleroma development environment](setting_up_pleroma_dev.md) +- [Setting up a Gitlab Runner](setting_up_a_gitlab_runner.md) +- [Authentication & Authorization](authentication_authorization.md) +- [ActivityPub Extensions](ap_extensions.md) +- [Mox Testing Guide](mox_testing.md)diff --git a/docs/development/mox_testing.md b/docs/development/mox_testing.md@@ -0,0 +1,485 @@ +# Using Mox for Testing in Pleroma + +## Introduction + +This guide explains how to use [Mox](https://hexdocs.pm/mox/Mox.html) for testing in Pleroma and how to migrate existing tests from Mock/meck to Mox. Mox is a library for defining concurrent mocks in Elixir that offers several key advantages: + +- **Async-safe testing**: Mox supports concurrent testing with `async: true` +- **Explicit contract through behaviors**: Enforces implementation of behavior callbacks +- **No module redefinition**: Avoids runtime issues caused by redefining modules +- **Expectations scoped to the current process**: Prevents test state from leaking between tests + +## Why Migrate from Mock/meck to Mox? + +### Problems with Mock/meck + +1. **Not async-safe**: Tests using Mock/meck cannot safely run with `async: true`, which slows down the test suite +2. **Global state**: Mocked functions are global, leading to potential cross-test contamination +3. **No explicit contract**: No guarantee that mocked functions match the actual implementation +4. **Module redefinition**: Can lead to hard-to-debug runtime issues + +### Benefits of Mox + +1. **Async-safe testing**: Tests can run concurrently with `async: true`, significantly speeding up the test suite +2. **Process isolation**: Expectations are set per process, preventing leakage between tests +3. **Explicit contracts via behaviors**: Ensures mocks implement all required functions +4. **Compile-time checks**: Prevents mocking non-existent functions +5. **No module redefinition**: Mocks are defined at compile time, not runtime + +## Existing Mox Setup in Pleroma + +Pleroma already has a basic Mox setup in the `Pleroma.DataCase` module, which handles some common mocking scenarios automatically. Here's what's included: + +### Default Mox Configuration + +The `setup` function in `DataCase` does the following: + +1. Sets up Mox for either async or non-async tests +2. Verifies all mock expectations on test exit +3. Stubs common dependencies with their real implementations + +```elixir +# From test/support/data_case.ex +setup tags do + setup_multi_process_mode(tags) + setup_streamer(tags) + stub_pipeline() + + Mox.verify_on_exit!() + + :ok +end +``` + +### Async vs. Non-Async Test Setup + +Pleroma configures Mox differently depending on whether your test is async or not: + +```elixir +def setup_multi_process_mode(tags) do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) + + if tags[:async] do + # For async tests, use process-specific mocks and stub CachexMock with NullCache + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + Mox.set_mox_private() + else + # For non-async tests, use global mocks and stub CachexMock with CachexProxy + Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()}) + + Mox.set_mox_global() + Mox.stub_with(Pleroma.CachexMock, Pleroma.CachexProxy) + clear_cachex() + end + + :ok +end +``` + +### Default Pipeline Stubs + +Pleroma automatically stubs several core components with their real implementations: + +```elixir +def stub_pipeline do + Mox.stub_with(Pleroma.Web.ActivityPub.SideEffectsMock, Pleroma.Web.ActivityPub.SideEffects) + Mox.stub_with(Pleroma.Web.ActivityPub.ObjectValidatorMock, Pleroma.Web.ActivityPub.ObjectValidator) + Mox.stub_with(Pleroma.Web.ActivityPub.MRFMock, Pleroma.Web.ActivityPub.MRF) + Mox.stub_with(Pleroma.Web.ActivityPub.ActivityPubMock, Pleroma.Web.ActivityPub.ActivityPub) + Mox.stub_with(Pleroma.Web.FederatorMock, Pleroma.Web.Federator) + Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Test.StaticConfig) + Mox.stub_with(Pleroma.StubbedHTTPSignaturesMock, Pleroma.Test.HTTPSignaturesProxy) +end +``` + +This means that by default, these mocks will behave like their real implementations unless you explicitly override them with expectations in your tests. + +### Understanding Config Mock Types + +Pleroma has three different Config mock implementations, each with a specific purpose and different characteristics regarding async test safety: + +#### 1. ConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.ConfigMock, for: Pleroma.Config.Getting)` +- It's stubbed with the real `Pleroma.Config` by default in `DataCase`: `Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config)` +- This means it falls back to the normal configuration behavior unless explicitly overridden +- Used for general mocking of configuration in tests where you want most config to behave normally +- ⚠️ **NOT ASYNC-SAFE**: Since it's stubbed with the real `Pleroma.Config`, it modifies global application state +- Can not be used in tests with `async: true` + +#### 2. StaticStubbedConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.StaticStubbedConfigMock, for: Pleroma.Config.Getting)` +- It's stubbed with `Pleroma.Test.StaticConfig` (defined in `test/test_helper.exs`) +- `Pleroma.Test.StaticConfig` creates a completely static configuration snapshot at the start of the test run: + ```elixir + defmodule Pleroma.Test.StaticConfig do + @moduledoc """ + This module provides a Config that is completely static, built at startup time from the environment. + It's safe to use in testing as it will not modify any state. + """ + + @behaviour Pleroma.Config.Getting + @config Application.get_all_env(:pleroma) + + def get(path, default \\ nil) do + get_in(@config, path) || default + end + end + ``` +- Configuration is frozen at startup time and doesn't change during the test run +- ✅ **ASYNC-SAFE**: Never modifies global state since it uses a frozen snapshot of the configuration + +#### 3. UnstubbedConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.UnstubbedConfigMock, for: Pleroma.Config.Getting)` +- Unlike the other two mocks, it's not automatically stubbed with any implementation in `DataCase` +- Starts completely "unstubbed" and requires tests to explicitly set expectations or stub it +- The most commonly used configuration mock in the test suite +- Often aliased as `ConfigMock` in individual test files: `alias Pleroma.UnstubbedConfigMock, as: ConfigMock` +- Set as the default config implementation in `config/test.exs`: `config :pleroma, :config_impl, Pleroma.UnstubbedConfigMock` +- Offers maximum flexibility for tests that need precise control over configuration values +- ✅ **ASYNC-SAFE**: Safe if used with `expect()` to set up test-specific expectations (since expectations are process-scoped) + +#### Configuring Components to Use Specific Mocks + +In `config/test.exs`, different components can be configured to use different configuration mocks: + +```elixir +# Components using UnstubbedConfigMock +config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.User.Backup, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock + +# Components using StaticStubbedConfigMock (async-safe) +config :pleroma, Pleroma.Language.LanguageDetector, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Web.Plugs.HTTPSecurityPlug, config_impl: Pleroma.StaticStubbedConfigMock +``` + +This allows different parts of the application to use the most appropriate configuration mocking strategy based on their specific needs. + +#### When to Use Each Config Mock Type + +- **ConfigMock**: ⚠️ For non-async tests only, when you want most configuration to behave normally with occasional overrides +- **StaticStubbedConfigMock**: ✅ For async tests where modifying global state would be problematic and a static configuration is sufficient +- **UnstubbedConfigMock**: ⚠️ Use carefully in async tests; set specific expectations rather than stubbing with implementations that modify global state + +#### Summary of Async Safety + +| Mock Type | Async-Safe? | Best Use Case | +|-----------|-------------|--------------| +| ConfigMock | ❌ No | Non-async tests that need minimal configuration overrides | +| StaticStubbedConfigMock | ✅ Yes | Async tests that need configuration values without modification | +| UnstubbedConfigMock | ⚠️ Depends | Any test with careful usage; set expectations rather than stubbing | + +## Configuration in Async Tests + +### Understanding `clear_config` Limitations + +The `clear_config` helper is commonly used in Pleroma tests to modify configuration for specific tests. However, it's important to understand that **`clear_config` is not async-safe** and should not be used in tests with `async: true`. + +Here's why: + +```elixir +# Implementation of clear_config in test/support/helpers.ex +defmacro clear_config(config_path, temp_setting) do + quote do + clear_config(unquote(config_path)) do + Config.put(unquote(config_path), unquote(temp_setting)) + end + end +end + +defmacro clear_config(config_path, do: yield) do + quote do + initial_setting = Config.fetch(unquote(config_path)) + + unquote(yield) + + on_exit(fn -> + case initial_setting do + :error -> + Config.delete(unquote(config_path)) + + {:ok, value} -> + Config.put(unquote(config_path), value) + end + end) + + :ok + end +end +``` + +The issue is that `clear_config`: +1. Modifies the global application environment +2. Uses `on_exit` to restore the original value after the test +3. Can lead to race conditions when multiple async tests modify the same configuration + +### Async-Safe Configuration Approaches + +When writing async tests with Mox, use these approaches instead of `clear_config`: + +1. **Dependency Injection with Module Attributes**: + ```elixir + # In your module + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + + def some_function do + value = @config_impl.get([:some, :config]) + # ... + end + ``` + +2. **Mock the Config Module**: + ```elixir + # In your test + Pleroma.ConfigMock + |> expect(:get, fn [:some, :config] -> "test_value" end) + ``` + +3. **Use Test-Specific Implementations**: + ```elixir + # Define a test-specific implementation + defmodule TestConfig do + def get([:some, :config]), do: "test_value" + def get(_), do: nil + end + + # In your test + Mox.stub_with(Pleroma.ConfigMock, TestConfig) + ``` + +4. **Pass Configuration as Arguments**: + ```elixir + # Refactor functions to accept configuration as arguments + def some_function(config \\ nil) do + config = config || Pleroma.Config.get([:some, :config]) + # ... + end + + # In your test + some_function("test_value") + ``` + +By using these approaches, you can safely run tests with `async: true` without worrying about configuration conflicts. + +## Setting Up Mox in Pleroma + +### Step 1: Define a Behavior + +Start by defining a behavior for the module you want to mock. This specifies the contract that both the real implementation and mocks must follow. + +```elixir +# In your implementation module (e.g., lib/pleroma/uploaders/s3.ex) +defmodule Pleroma.Uploaders.S3.ExAwsAPI do + @callback request(op :: ExAws.Operation.t()) :: {:ok, ExAws.Operation.t()} | {:error, term()} +end +``` + +### Step 2: Make Your Implementation Configurable + +Modify your module to use a configurable implementation. This allows for dependency injection and easier testing. + +```elixir +# In your implementation module +@ex_aws_impl Application.compile_env(:pleroma, [__MODULE__, :ex_aws_impl], ExAws) +@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + +def put_file(%Pleroma.Upload{} = upload) do + # Use @ex_aws_impl instead of ExAws directly + case @ex_aws_impl.request(op) do + {:ok, _} -> + {:ok, {:file, s3_name}} + + error -> + Logger.error("#{__MODULE__}: #{inspect(error)}") + error + end +end +``` + +### Step 3: Define the Mock in test/support/mocks.ex + +Add your mock definition in the central mocks file: + +```elixir +# In test/support/mocks.ex +Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) +``` + +### Step 4: Configure the Mock in Test Environment + +In your test configuration (e.g., `config/test.exs`), specify which mock implementation to use: + +```elixir +config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock +config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock +``` + +## Writing Tests with Mox + +### Setting Up Your Test + +```elixir +defmodule Pleroma.Uploaders.S3Test do + use Pleroma.DataCase, async: true # Note: async: true is now possible! + + alias Pleroma.Uploaders.S3 + alias Pleroma.Uploaders.S3.ExAwsMock + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + + import Mox # Import Mox functions + + # Note: verify_on_exit! is already called in DataCase setup + # so you don't need to add it explicitly in your test module +end +``` + +### Setting Expectations with Mox + +Mox uses an explicit expectation system. Here's how to use it: + +```elixir +# Basic expectation for a function call +ExAwsMock +|> expect(:request, fn _req -> {:ok, %{status_code: 200}} end) + +# Expectation for multiple calls with same response +ExAwsMock +|> expect(:request, 3, fn _req -> {:ok, %{status_code: 200}} end) + +# Expectation with specific arguments +ExAwsMock +|> expect(:request, fn %{bucket: "test_bucket"} -> {:ok, %{status_code: 200}} end) + +# Complex configuration mocking +ConfigMock +|> expect(:get, fn key -> + [ + {Pleroma.Upload, [uploader: Pleroma.Uploaders.S3, base_url: "https://s3.amazonaws.com"]}, + {Pleroma.Uploaders.S3, [bucket: "test_bucket"]} + ] + |> get_in(key) +end) +``` + +### Understanding Mox Modes in Pleroma + +Pleroma's DataCase automatically configures Mox differently based on whether your test is async or not: + +1. **Async tests** (`async: true`): + - Uses `Mox.set_mox_private()` - expectations are scoped to the current process + - Stubs `Pleroma.CachexMock` with `Pleroma.NullCache` + - Each test process has its own isolated mock expectations + +2. **Non-async tests** (`async: false`): + - Uses `Mox.set_mox_global()` - expectations are shared across processes + - Stubs `Pleroma.CachexMock` with `Pleroma.CachexProxy` + - Mock expectations can be set in one process and called from another + +Choose the appropriate mode based on your test requirements. For most tests, async mode is preferred for better performance. + +## Migrating from Mock/meck to Mox + +Here's a step-by-step guide for migrating existing tests from Mock/meck to Mox: + +### 1. Identify the Module to Mock + +Look for `with_mock` or `test_with_mock` calls in your tests: + +```elixir +# Old approach with Mock +with_mock ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} +end +``` + +### 2. Define a Behavior for the Module + +Create a behavior that defines the functions you want to mock: + +```elixir +defmodule Pleroma.Uploaders.S3.ExAwsAPI do + @callback request(op :: ExAws.Operation.t()) :: {:ok, ExAws.Operation.t()} | {:error, term()} +end +``` + +### 3. Update Your Implementation to Use a Configurable Dependency + +```elixir +# Old +def put_file(%Pleroma.Upload{} = upload) do + case ExAws.request(op) do + # ... + end +end + +# New +@ex_aws_impl Application.compile_env(:pleroma, [__MODULE__, :ex_aws_impl], ExAws) + +def put_file(%Pleroma.Upload{} = upload) do + case @ex_aws_impl.request(op) do + # ... + end +end +``` + +### 4. Define the Mock in mocks.ex + +```elixir +Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) +``` + +### 5. Configure the Test Environment + +```elixir +config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock +``` + +### 6. Update Your Tests to Use Mox + +```elixir +# Old (with Mock) +test_with_mock "save file", ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} + assert_called(ExAws.request(:_)) +end + +# New (with Mox) +test "save file" do + ExAwsMock + |> expect(:request, fn _req -> {:ok, %{status_code: 200}} end) + + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} +end +``` + +### 7. Enable Async Testing + +Now you can safely enable `async: true` in your test module: + +```elixir +use Pleroma.DataCase, async: true +``` + +## Best Practices + +1. **Always define behaviors**: They serve as contracts and documentation +2. **Keep mocks in a central location**: Use test/support/mocks.ex for all mock definitions +3. **Use verify_on_exit!**: This is already set up in DataCase, ensuring all expected calls were made +4. **Use specific expectations**: Be as specific as possible with your expectations +5. **Enable async: true**: Take advantage of Mox's concurrent testing capability +6. **Don't over-mock**: Only mock external dependencies that are difficult to test directly +7. **Leverage existing stubs**: Use the default stubs provided by DataCase when possible +8. **Avoid clear_config in async tests**: Use dependency injection and mocking instead + +## Example: Complete Migration + +For a complete example of migrating a test from Mock/meck to Mox, you can refer to commit `90a47ca050c5839e8b4dc3bac315dc436d49152d` in the Pleroma repository, which shows how the S3 uploader tests were migrated. + +## Conclusion + +Migrating tests from Mock/meck to Mox provides significant benefits for the Pleroma test suite, including faster test execution through async testing, better isolation between tests, and more robust mocking through explicit contracts. By following this guide, you can successfully migrate existing tests and write new tests using Mox. +\ No newline at end of filediff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md@@ -69,12 +69,18 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen` +* Generate the configuration: + +```shell +sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen +``` + +* During this process: * Answer with `yes` if it asks you to install `rebar3`. * This may take some time, because parts of pleroma get compiled first. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. -* Check the configuration and if all looks right, rename it, so Pleroma will load it (`prod.secret.exs` for productive instance, `dev.secret.exs` for development instances): +* Check the configuration and if all looks right, rename it, so Pleroma will load it (`prod.secret.exs` for production instances, `dev.secret.exs` for development instances): ```shell sudo -Hu pleroma mv config/{generated_config.exs,prod.secret.exs}diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md@@ -14,7 +14,7 @@ Note: This article is potentially outdated because at this time we may not have - PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) - `postgresql-contrib` 11.0以上 (同上) -- Elixir 1.13 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) +- Elixir 1.14 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) - `erlang-dev` - `erlang-nox` - `git`diff --git a/docs/installation/freebsd_en.md b/docs/installation/freebsd_en.md@@ -31,7 +31,7 @@ Setup the required services to automatically start at boot, using `sysrc(8)`. ### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)) ```shell -# pkg install imagemagick ffmpeg p5-Image-ExifTool +# pkg install imagemagick ffmpeg p5-Image-ExifTool vips ``` ## Configuring Pleromadiff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include@@ -1,8 +1,8 @@ ## Required dependencies * PostgreSQL >=11.0 -* Elixir >=1.13.0 <1.17 -* Erlang OTP >=22.2.0 (supported: <27) +* Elixir >=1.14.0 <1.19 +* Erlang OTP >=23.0.0 (supported: <28) * git * file / libmagic * gcc or clangdiff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md@@ -1,25 +1,29 @@ # Installing on OpenBSD -This guide describes the installation and configuration of pleroma (and the required software to run it) on a single OpenBSD 6.6 server. +{! backend/installation/otp_vs_from_source_source.include !} + +This guide describes the installation and configuration of Pleroma (and the required software to run it) on a single OpenBSD 7.7 server. For any additional information regarding commands and configuration files mentioned here, check the man pages [online](https://man.openbsd.org/) or directly on your server with the man command. {! backend/installation/generic_dependencies.include !} +## Installation + ### Preparing the system #### Required software -To install them, run the following command (with doas or as root): +To install required packages, run the following command: ``` -pkg_add elixir gmake git postgresql-server postgresql-contrib cmake ffmpeg ImageMagick +# pkg_add elixir gmake git postgresql-server postgresql-contrib cmake libmagic libvips ``` -Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. +Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). +Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. #### Optional software -Per [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md): * ImageMagick * ffmpeg * exiftool @@ -27,234 +31,351 @@ Per [`docs/installation/optional/media_graphics_packages.md`](../installation/op To install the above: ``` -pkg_add ImageMagick ffmpeg p5-Image-ExifTool +# pkg_add ImageMagick ffmpeg p5-Image-ExifTool ``` -#### Creating the pleroma user -Pleroma will be run by a dedicated user, \_pleroma. Before creating it, insert the following lines in login.conf: +For more information read [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md): + +### PostgreSQL + +Switch to the \_postgresql user and initialize PostgreSQL: + ``` -pleroma:\ - :datasize-max=1536M:\ - :datasize-cur=1536M:\ - :openfiles-max=4096 +# su _postgresql +$ initdb -D /var/postgresql/data -U postgres --encoding=utf-8 --lc-collate=C ``` -This creates a "pleroma" login class and sets higher values than default for datasize and openfiles (see [login.conf(5)](https://man.openbsd.org/login.conf)), this is required to avoid having pleroma crash some time after starting. -Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma` +Running PostgreSQL in a different directory than `/var/postgresql/data` requires changing the `daemon_flags` variable in the `/etc/rc.d/postgresql` script. -#### Clone pleroma's directory -Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. +For security reasons it is recommended to change the authentication method for `local` and `host` connections with the localhost address to `scram-sha-256`.<br> +Do not forget to set a password for the `postgres` user before doing so, otherwise you won't be able to log back in unless you change the authentication method back to `trust`.<br> +Changing the password hashing algorithm is not needed.<br> +For more information [read](https://www.postgresql.org/docs/16/auth-pg-hba-conf.html) the PostgreSQL documentation. -#### PostgreSQL -Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: -You will need to specify pgdata directory to the default (/var/postgresql/data) with the `-D <path>` and set the user to postgres with the `-U <username>` flag. This can be done as follows: +Enable and start the postgresql service: ``` -initdb -D /var/postgresql/data -U postgres +# rcctl enable postgresql +# rcctl start postgresql ``` -If you are not using the default directory, you will have to update the `datadir` variable in the /etc/rc.d/postgresql script. -When this is done, enable postgresql so that it starts on boot and start it. As root, run: +To check that PostgreSQL started properly and didn't fail right after starting, run `# rcctl check postgresql` which should return `postgresql(ok)`. + +### Configuring Pleroma + +Pleroma will be run by a dedicated \_pleroma user. Before creating it, insert the following lines in `/etc/login.conf`: + ``` -rcctl enable postgresql -rcctl start postgresql +pleroma:\ + :datasize=1536M:\ + :openfiles-max=4096:\ + :openfiles-cur=1024:\ + :setenv=LC_ALL=en_US.UTF-8,VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS,MIX_ENV=prod:\ + :tc=daemon: ``` -To check that it started properly and didn't fail right after starting, you can run `ps aux | grep postgres`, there should be multiple lines of output. -#### httpd -httpd will have three functions: +This creates a "pleroma" login class and sets higher values than default for datasize and openfiles (see [login.conf(5)](https://man.openbsd.org/login.conf)), this is required to avoid having Pleroma crash some time after starting. - * redirect requests trying to reach the instance over http to the https URL - * serve a robots.txt file - * get Let's Encrypt certificates, with acme-client +Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): + +``` +# useradd -m -L pleroma _pleroma +``` + +Switch to the _pleroma user: -Insert the following config in httpd.conf: ``` -# $OpenBSD: httpd.conf,v 1.17 2017/04/16 08:50:49 ajacoutot Exp $ +# su -l _pleroma +``` -ext_inet="<IPv4 address>" -ext_inet6="<IPv6 address>" +Clone the Pleroma repository: -server "default" { - listen on $ext_inet port 80 # Comment to disable listening on IPv4 - listen on $ext_inet6 port 80 # Comment to disable listening on IPv6 - listen on 127.0.0.1 port 80 # Do NOT comment this line +``` +$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git +$ cd pleroma +``` - log syslog - directory no index +Pleroma is now installed in /home/\_pleroma/pleroma/. To configure it run: - location "/.well-known/acme-challenge/*" { - root "/acme" - request strip 2 - } +``` +$ mix deps.get +$ MIX_ENV=prod mix pleroma.instance gen # You will be asked a few questions here. +$ cp config/generated_config.exs config/prod.secret.exs +``` - location "/robots.txt" { root "/htdocs/local/" } - location "/*" { block return 302 "https://$HTTP_HOST$REQUEST_URI" } -} +Note: Answer yes when asked to install Hex and rebar3. This step might take some time as Pleroma gets compiled first. + +Create the Pleroma database: -types { -} ``` -Do not forget to change *<IPv4/6 address\>* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options. +$ psql -U postgres -f config/setup_db.psql +``` + +Apply database migrations: -Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt. -Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root): ``` -rcctl enable httpd -rcctl start httpd +$ MIX_ENV=prod mix ecto.migrate ``` -#### acme-client +Note: You will need to run this step again when updating your instance to a newer version with `git pull` or `git checkout tags/NEW_VERSION`. + +As \_pleroma in /home/\_pleroma/pleroma, you can now run `MIX_ENV=prod mix phx.server` to start your instance. +In another SSH session or a tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. +Double-check that the *uri* value near the bottom is your instance's domain name and the instance *title* are correct. + +### Configuring acme-client + acme-client is used to get SSL/TLS certificates from Let's Encrypt. -Insert the following configuration in /etc/acme-client.conf: +Insert the following configuration in `/etc/acme-client.conf` and replace `example.tld` with your domain: + ``` # -# $OpenBSD: acme-client.conf,v 1.4 2017/03/22 11:14:14 benno Exp $ +# $OpenBSD: acme-client.conf,v 1.5 2023/05/10 07:34:57 tb Exp $ # -authority letsencrypt-<domain name> { - #agreement url "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf" - api url "https://acme-v02.api.letsencrypt.org/directory" - account key "/etc/acme/letsencrypt-privkey-<domain name>.pem" +authority letsencrypt { + api url "https://acme-v02.api.letsencrypt.org/directory" + account key "/etc/acme/letsencrypt-privkey.pem" } -domain <domain name> { - domain key "/etc/ssl/private/<domain name>.key" - domain certificate "/etc/ssl/<domain name>.crt" - domain full chain certificate "/etc/ssl/<domain name>.fullchain.pem" - sign with letsencrypt-<domain name> - challengedir "/var/www/acme/" +domain example.tld { + # Adds alternative names to the certificate. Useful when serving media on another domain. Comma or space separated list. + # alternative names { } + + domain key "/etc/ssl/private/example.tld.key" + domain certificate "/etc/ssl/example.tld_cert-only.crt" + domain full chain certificate "/etc/ssl/example.tld.crt" + sign with letsencrypt } ``` -Replace *<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` to create account and domain keys, and request a certificate for the first time. -Make acme-client run everyday by adding it in /etc/daily.local. As root, run the following command: `echo "acme-client <domain name>" >> /etc/daily.local`. -Relayd will look for certificates and keys based on the address it listens on (see next part), the easiest way to make them available to relayd is to create a link, as root run: +Check the configuration: + ``` -ln -s /etc/ssl/<domain name>.fullchain.pem /etc/ssl/<IP address>.crt -ln -s /etc/ssl/private/<domain name>.key /etc/ssl/private/<IP address>.key +# acme-client -n ``` -This will have to be done for each IPv4 and IPv6 address relayd listens on. -#### relayd -relayd will be used as the reverse proxy sitting in front of pleroma. -Insert the following configuration in /etc/relayd.conf: -``` -# $OpenBSD: relayd.conf,v 1.4 2018/03/23 09:55:06 claudio Exp $ - -ext_inet="<IPv4 address>" -ext_inet6="<IPv6 address>" - -table <pleroma_server> { 127.0.0.1 } -table <httpd_server> { 127.0.0.1 } - -http protocol plerup { # Protocol for upstream pleroma server - #tcp { nodelay, sack, socket buffer 65536, backlog 128 } # Uncomment and adjust as you see fit - tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305" - tls ecdhe secp384r1 - - # Forward some paths to the local server (as pleroma won't respond to them as you might want) - pass request quick path "/robots.txt" forward to <httpd_server> - - # Append a bunch of headers - match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # This two header and the next one are not strictly required by pleroma but adding them won't hurt - match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT" - - match response header append "X-XSS-Protection" value "1; mode=block" - match response header append "X-Permitted-Cross-Domain-Policies" value "none" - match response header append "X-Frame-Options" value "DENY" - match response header append "X-Content-Type-Options" value "nosniff" - match response header append "Referrer-Policy" value "same-origin" - match response header append "X-Download-Options" value "noopen" - match response header append "Content-Security-Policy" value "default-src 'none'; base-uri 'self'; form-action 'self'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://CHANGEME.tld; upgrade-insecure-requests;" # Modify "CHANGEME.tld" and set your instance's domain here - match request header append "Connection" value "upgrade" - #match response header append "Strict-Transport-Security" value "max-age=31536000; includeSubDomains" # Uncomment this only after you get HTTPS working. - - # If you do not want remote frontends to be able to access your Pleroma backend server, comment these lines - match response header append "Access-Control-Allow-Origin" value "*" - match response header append "Access-Control-Allow-Methods" value "POST, PUT, DELETE, GET, PATCH, OPTIONS" - match response header append "Access-Control-Allow-Headers" value "Authorization, Content-Type, Idempotency-Key" - match response header append "Access-Control-Expose-Headers" value "Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id" - # Stop commenting lines here +### Configuring the Web server + +Pleroma supports two Web servers: + + * nginx (recommended for most users) + * OpenBSD's httpd and relayd (ONLY for advanced users, media proxy cache is NOT supported and will NOT work properly) + +#### nginx + +Since nginx is not installed by default, install it by running: + +``` +# pkg_add nginx +``` + +Add the following to `/etc/nginx/nginx.conf`, within the `server {}` block listening on port 80 and change `server_name`, as follows: + +``` +http { + ... + + server { + ... + server_name localhost; # Replace with your domain + + location /.well-known/acme-challenge { + rewrite ^/\.well-known/acme-challenge/(.*) /$1 break; + root /var/www/acme; + } + } } +``` + +Start the nginx service and acquire certificates: + +``` +# rcctl start nginx +# acme-client example.tld +``` + +Add certificate auto-renewal by adding acme-client to `/etc/weekly.local`, replace `example.tld` with your domain: + +``` +# echo "acme-client example.tld && rcctl reload nginx" >> /etc/weekly.local +``` -relay wwwtls { - listen on $ext_inet port https tls # Comment to disable listening on IPv4 - listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 +OpenBSD's default nginx configuration does not contain an include directive, which is typically used for multiple sites. +Therefore, you will need to first create the required directory as follows: - protocol plerup +``` +# mkdir /etc/nginx/sites-available +# mkdir /etc/nginx/sites-enabled +``` + +Next add the `include` directive to `/etc/nginx/nginx.conf`, within the `http {}` block, as follows: + +``` +http { + ... - forward to <pleroma_server> port 4000 check http "/" code 200 - forward to <httpd_server> port 80 check http "/robots.txt" code 200 + server { + ... + } + + include /etc/nginx/sites-enabled/*; } ``` -Again, change *<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://<your instance's domain name\>*. -Check the configuration with `relayd -n`, if it is OK enable and start relayd (as root): + +As root, copy `/home/_pleroma/pleroma/installation/pleroma.nginx` to `/etc/nginx/sites-available/pleroma.nginx`. + +Edit default `/etc/nginx/sites-available/pleroma.nginx` settings and replace `example.tld` with your domain: + + * Uncomment the location block for `~ /\.well-known/acme-challenge` in the server block listening on port 80 + - add `rewrite ^/\.well-known/acme-challenge/(.*) /$1 break;` above the `root` location + - change the `root` location to `/var/www/acme;` + * Change `ssl_trusted_certificate` to `/etc/ssl/example.tld_cert-only.crt` + * Change `ssl_certificate` to `/etc/ssl/example.tld.crt` + * Change `ssl_certificate_key` to `/etc/ssl/private/example.tld.key` + +Remove the following `location {}` block from `/etc/nginx/nginx.conf`, that was previously added for acquiring certificates and change `server_name` back to `localhost`: + ``` -rcctl enable relayd -rcctl start relayd +http { + ... + + server { + ... + server_name example.tld; # Change back to localhost + + # Delete this block + location /.well-known/acme-challenge { + rewrite ^/\.well-known/acme-challenge/(.*) /$1 break; + root /var/www/acme; + } + } +} ``` -##### (Strongly recommended) serve media on another domain +Symlink the Pleroma configuration to the enabled sites: -Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. +``` +# ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled +``` + +Check nginx configuration syntax by running: + +``` +# nginx -t +``` + +Note: If the above command complains about a `conflicting server name`, check again that the `location {}` block for acquiring certificates has been removed from `/etc/nginx/nginx.conf` and that the `server_name` has been reverted back to `localhost`. +After doing so run `# nginx -t` again. + +If the configuration is correct, you can now enable and reload the nginx service: + +``` +# rcctl enable nginx +# rcctl reload nginx +``` + +#### httpd + +***Skip this section when using nginx*** + +httpd will have two functions: + + * redirect requests trying to reach the instance over http to the https URL + * get Let's Encrypt certificates, with acme-client + +As root, copy `/home/_pleroma/pleroma/installation/openbsd/httpd.conf` to `/etc/httpd.conf`, or modify the existing one. + +Edit `/etc/httpd.conf` settings and change: + + * `<ipaddr>` with your instance's IPv4 address + * All occurrences of `example.tld` with your instance's domain name + * When using IPv6 also change: + - Uncomment the `ext_inet6="<ip6addr>"` line near the beginning of the file and change `<ip6addr` to your instance's IPv6 address + - Uncomment the line starting with `listen on $ext_inet6` in the `server` block -#### pf -Enabling and configuring pf is highly recommended. -In /etc/pf.conf, insert the following configuration: +Check the configuration by running: +``` +# httpd -n ``` -# Macros -if="<network interface>" -authorized_ssh_clients="any" -# Skip traffic on loopback interface -set skip on lo +If the configuration is correct, enable and start the `httpd` service: -# Default behavior -set block-policy drop -block in log all -pass out quick +``` +# rcctl enable httpd +# rcctl start httpd +``` -# Security features -match in all scrub (no-df random-id) -block in log from urpf-failed +Acquire certificate: -# Rules -pass in quick on $if inet proto icmp to ($if) icmp-type { echoreq unreach paramprob trace } # ICMP -pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach paramprob timex toobig } # ICMPv6 -pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd -pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh ``` -Replace *<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for example, your home IP address, to avoid SSH connection attempts from bots. +# acme-client example.tld +``` -Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`. +#### relayd + +***Skip this section when using nginx*** + +relayd will be used as the reverse proxy sitting in front of pleroma. -#### Configure and start pleroma -Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`). +As root, copy `/home/_pleroma/pleroma/installation/openbsd/relayd.conf` to `/etc/relayd.conf`, or modify the existing one. -Then follow the main installation guide: +Edit `/etc/relayd.conf` settings and change: - * run `mix deps.get` - * run `MIX_ENV=prod mix pleroma.instance gen` and enter your instance's information when asked - * copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK. - * exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/pleroma/config/setup_db.psql` to setup the database. - * return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate` + * `<ipaddr>` with your instance's IPv4 address + * All occurrences of `example.tld` with your instance's domain name + * When using IPv6 also change: + - Uncomment the `ext_inet6="<ip6addr>"` line near the beginning of the file and change `<ip6addr>` to your instance's IPv6 address + - Uncomment the line starting with `listen on $ext_inet6` in the `relay wwwtls` block + +Check the configuration by running: +``` +# relayd -n +``` + +If the configuration is correct, enable and start the `relayd` service: + +``` +# rcctl enable relayd +# rcctl start relayd +``` -As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance. -In another SSH session/tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. Double-check that *uri*'s value is your instance's domain name. +Add certificate auto-renewal by adding acme-client to `/etc/weekly.local`, replace `example.tld` with your domain: -##### Starting pleroma at boot -An rc script to automatically start pleroma at boot hasn't been written yet, it can be run in a tmux session (tmux is in base). +``` +# echo "acme-client example.tld && rcctl reload relayd" >> /etc/weekly.local +``` + +#### (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + +### Starting pleroma at boot + +Copy the startup script and make sure it's executable: + +``` +# cp /home/_pleroma/pleroma/installation/openbsd/rc.d/pleroma /etc/rc.d/pleroma +# chmod 555 /etc/rc.d/pleroma +``` + +Enable and start the pleroma service: + +``` +# rcctl enable pleroma +# rcctl start pleroma +``` +### Create administrative user -#### Create administrative user +If your instance is up and running, you can create your first user with administrative rights with the following commands as the \_pleroma user: -If your instance is up and running, you can create your first user with administrative rights with the following command as the \_pleroma user. ``` -LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin +$ cd pleroma +$ MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin ``` -#### Further reading +### Further reading {! backend/installation/further_reading.include !}diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md@@ -4,7 +4,7 @@ Note: This article is potentially outdated because at this time we may not have Tarvitset: * Oman domainin -* OpenBSD 6.3 -serverin +* OpenBSD 7.5 -serverin * Auttavan ymmärryksen unix-järjestelmistä Komennot, joiden edessä on '#', tulee ajaa käyttäjänä `root`. Tämä on @@ -18,7 +18,7 @@ Matrix-kanava #pleroma:libera.chat ovat hyviä paikkoja löytää apua Asenna tarvittava ohjelmisto: -`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake ffmpeg ImageMagick` +`# pkg_add git elixir gmake postgresql-server postgresql-contrib cmake libmagic libvips` #### Optional softwarediff --git a/installation/freebsd/rc.d/pleroma b/installation/freebsd/rc.d/pleroma@@ -24,4 +24,6 @@ command=/usr/local/bin/elixir command_args="--erl \"-detached\" -S /usr/local/bin/mix phx.server" procname="*beam.smp" +PATH="${PATH}:/usr/local/sbin:/usr/local/bin" + run_rc_command "$1"diff --git a/installation/openbsd/httpd.conf b/installation/openbsd/httpd.conf@@ -2,20 +2,21 @@ # Default httpd.conf file for Pleroma on OpenBSD # Simple installation instructions # 1. Place file in /etc -# 2. Replace <IPv4 address> with your public IP address -# 3. If using IPv6, uncomment IPv6 lines and replace <IPv6 address> with your public IPv6 address -# 4. Check file using 'doas httpd -n' -# 5. Enable and start httpd: +# 2. Replace <ipaddr> with your public IP address +# 3. If using IPv6, uncomment IPv6 lines and replace <ip6addr> with your public IPv6 address +# 4. Replace all occurences of example.tld with your instance's domain name. +# 5. Check file using 'doas httpd -n' +# 6. Enable and start httpd: # # doas rcctl enable httpd # # doas rcctl start httpd # -ext_inet="<IPv4 address>" -#ext_inet6="<IPv6 address>" +ext_inet="<ipaddr>" +#ext_inet6="<ip6addr>" -server "default" { +server "example.tld" { listen on $ext_inet port 80 # Comment to disable listening on IPv4 -# listen on $ext_inet6 port 80 # Comment to disable listening on IPv6 + #listen on $ext_inet6 port 80 # Comment to disable listening on IPv6 listen on 127.0.0.1 port 80 # Do NOT comment this line log syslog @@ -26,10 +27,18 @@ server "default" { request strip 2 } - location "/robots.txt" { root "/htdocs/local/" } - location "/*" { block return 302 "https://$HTTP_HOST$REQUEST_URI" } + location "/*" { block return 301 "https://$HTTP_HOST$REQUEST_URI" } } +# Example of serving a basic static website besides Pleroma using the example configuration in relayd +#server "site.example.tld" { +# listen on 127.0.0.1 port 8080 +# +# location "/*" { +# root "/website" +# } +#} + types { include "/usr/share/misc/mime.types" }diff --git a/installation/openbsd/rc.d/pleroma b/installation/openbsd/rc.d/pleroma@@ -0,0 +1,31 @@ +#!/bin/ksh +# +# Default init file for Pleroma on OpenBSD +# +# Simple installation instructions: +# 1. Install Pleroma per wiki instructions +# 2. Place this pleroma file in /etc/rc.d +# 3. Enable and start Pleroma +# # doas rcctl enable pleroma +# # doas rcctl start pleroma +# + +daemon="/usr/local/bin/elixir" +daemon_flags="--erl \"-detached\" -S /usr/local/bin/mix phx.server" +daemon_user="_pleroma" +daemon_execdir="/home/_pleroma/pleroma" + +. /etc/rc.d/rc.subr + +rc_reload=NO +pexp="phx.server" + +rc_check() { + pgrep -q -U _pleroma -f "phx.server" +} + +rc_stop() { + pkill -q -U _pleroma -f "phx.server" +} + +rc_cmd $1diff --git a/installation/openbsd/rc.d/pleromad b/installation/openbsd/rc.d/pleromad@@ -1,34 +0,0 @@ -#!/bin/ksh -# -# Default init file for Pleroma on OpenBSD -# -# Simple installation instructions: -# 1. Install Pleroma per wiki instructions -# 2. Place this pleromad file in /etc/rc.d -# 3. Enable and start Pleroma -# # doas rcctl enable pleromad -# # doas rcctl start pleromad -# - -daemon="/usr/local/bin/elixir" -daemon_flags="--detached -S /usr/local/bin/mix phx.server" -daemon_user="_pleroma" - -. /etc/rc.d/rc.subr - -rc_reload=NO -pexp="phx.server" - -rc_check() { - pgrep -q -U _pleroma -f "phx.server" -} - -rc_start() { - ${rcexec} "cd pleroma; ${daemon} ${daemon_flags}" -} - -rc_stop() { - pkill -q -U _pleroma -f "phx.server" -} - -rc_cmd $1diff --git a/installation/openbsd/relayd.conf b/installation/openbsd/relayd.conf@@ -3,9 +3,10 @@ # Simple installation instructions: # 1. Place in /etc # 2. Replace <ipaddr> with your public IPv4 address -# 3. If using IPv6i, uncomment IPv6 lines and replace <ip6addr> with your public IPv6 address -# 4. Check file using 'doas relayd -n' -# 5. Reload/start relayd +# 3. If using IPv6, uncomment IPv6 lines and replace <ip6addr> with your public IPv6 address +# 4. Replace all occurrences of example.tld with your instance's domain +# 5. Check file using 'doas relayd -n' +# 6. Reload/start relayd # # doas rcctl enable relayd # # doas rcctl start relayd # @@ -14,31 +15,54 @@ ext_inet="<ipaddr>" #ext_inet6="<ip6addr>" table <pleroma_server> { 127.0.0.1 } -table <httpd_server> { 127.0.0.1 } -http protocol plerup { # Protocol for upstream pleroma server +# Uncomment when you want to serve other services than Pleroma. +# In this example tables are used only as way to differentiate between Pleroma and other services. +# Feel free to rename "httpd_server" everywhere to fit your setup. +#table <httpd_server> { 127.0.0.1 } + +http protocol pleroma { # Protocol for upstream Pleroma server #tcp { nodelay, sack, socket buffer 65536, backlog 128 } # Uncomment and adjust as you see fit - tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA0-POLY1305" - tls ecdhe secp384r1 + tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" + tls ecdhe "X25519,P-256,P-384,secp521r1" # relayd default+secp521r1 - # Forward some paths to the local server (as pleroma won't respond to them as you might want) - pass request quick path "/robots.txt" forward to <httpd_server> + return error - # Append a bunch of headers - match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # This two header and the next one are not strictl required by pleroma but adding them won't hurt - match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT" + # When serving multiple services with different certificates, specify multiple "tls keypair" keywords + # and add forwards to those services before the block keyword near the bottom of the protocol and relay configurations. + # The string in quotes must match the fullchain certificate file created by acme-client without the extension. + # For example: + # tls keypair "pleroma.example.tld" + # tls keypair "example.tld" + tls keypair "example.tld" + match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "Connection" value "upgrade" + # When hosting Pleroma on a subdomain, replace example.tld accordingly (not the base domain). + # From the above example, "example.tld" should be replaced with "pleroma.example.tld" instead. + pass request quick header "Host" value "example.tld" forward to <pleroma_server> + + # Uncomment when serving media uploads on a different (sub)domain. + # Keep media proxy disabled, as it will NOT work under relayd/httpd. If you want to also setup media proxy, use nginx instead. + #pass request quick header "Host" value "media.example.tld" forward to <pleroma_server> + + # When serving multiple services, add the forwards here. + # Example: + #pass request quick header "Host" value "example.tld" forward to <httpd_server> + + block } relay wwwtls { listen on $ext_inet port https tls # Comment to disable listening on IPv4 -# listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 + #listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 - protocol plerup + protocol pleroma - forward to <pleroma_server> port 4000 check http "/" code 200 - forward to <httpd_server> port 80 check http "/robots.txt" code 200 -} + forward to <pleroma_server> port 4000 check tcp timeout 500 # Adjust timeout accordingly when relayd returns 502 while Pleroma is running without problems. + # When serving multiple services, add the forwards here. + # Example: + #forward to <httpd_server> port 8080 +}diff --git a/installation/openldap/pw_self_service.ldif b/installation/openldap/pw_self_service.ldif@@ -0,0 +1,7 @@ +dn: olcDatabase={1}mdb,cn=config +changetype: modify +add: olcAccess +olcAccess: {1}to attrs=userPassword + by self write + by anonymous auth + by * nonediff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex@@ -26,7 +26,11 @@ defmodule Mix.Pleroma do Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) unless System.get_env("DEBUG") do - Logger.remove_backend(:console) + try do + Logger.remove_backend(:console) + catch + :exit, _ -> :ok + end end adapter = Application.get_env(:tesla, :adapter)diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex@@ -93,6 +93,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do ) files = fetch_and_decode!(files_loc) + files_to_unzip = for({_, f} <- files, do: f) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -103,17 +104,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do pack_name ]) - files_to_unzip = - Enum.map( - files, - fn {_, f} -> to_charlist(f) end - ) - - {:ok, _} = - :zip.unzip(binary_archive, - cwd: String.to_charlist(pack_path), - file_list: files_to_unzip - ) + {:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, pack_path, files_to_unzip) IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name])) @@ -201,7 +192,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}") - {:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir)) + {:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, tmp_pack_dir) emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex@@ -271,7 +271,7 @@ defmodule Mix.Tasks.Pleroma.Instance do [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) |> Enum.each(fn dir -> - File.mkdir_p!(dir) + Pleroma.Backports.mkdir_p!(dir) File.chmod!(dir, 0o700) end)diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxt do static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) + Pleroma.Backports.mkdir_p!(static_dir) end robots_txt_path = Path.join(static_dir, "robots.txt")diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex@@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do import Ecto.Query import Pleroma.Search.Meilisearch, - only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1] + only: [meili_put: 2, meili_get: 1, meili_delete: 1] def run(["index"]) do start_pleroma() @@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do end {:ok, _} = - meili_post( + meili_put( "/indexes/objects/settings/ranking-rules", [ "published:desc", @@ -42,7 +42,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do ) {:ok, _} = - meili_post( + meili_put( "/indexes/objects/settings/searchable-attributes", [ "content"diff --git a/lib/mix/tasks/pleroma/test_runner.ex b/lib/mix/tasks/pleroma/test_runner.ex@@ -4,7 +4,9 @@ defmodule Mix.Tasks.Pleroma.TestRunner do use Mix.Task def run(args \\ []) do - case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do + case System.cmd("mix", ["test", "--warnings-as-errors"] ++ args, + into: IO.stream(:stdio, :line) + ) do {_, 0} -> :okdiff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex@@ -43,9 +43,6 @@ defmodule Pleroma.Application do # every time the application is restarted, so we disable module # conflicts at runtime Code.compiler_options(ignore_module_conflict: true) - # Disable warnings_as_errors at runtime, it breaks Phoenix live reload - # due to protocol consolidation warnings - Code.compiler_options(warnings_as_errors: false) Pleroma.Telemetry.Logger.attach() Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() @@ -56,7 +53,10 @@ defmodule Pleroma.Application do Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() end - Pleroma.ApplicationRequirements.verify!() + if Config.get(:env) != :test do + Pleroma.ApplicationRequirements.verify!() + end + load_custom_modules() Pleroma.Docs.JSON.compile() limiters_setup() @@ -68,26 +68,11 @@ defmodule Pleroma.Application do Finch.start_link(name: MyFinch) end - if adapter == Tesla.Adapter.Gun do - if version = Pleroma.OTPVersion.version() do - [major, minor] = - version - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> Enum.take(2) - - if (major == 22 and minor < 2) or major < 22 do - raise " - !!!OTP VERSION WARNING!!! - You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2. - " - end - else - raise " - !!!OTP VERSION WARNING!!! - To support correct handling of unordered certificates chains - OTP version must be > 22.2. - " - end + # Disable warnings_as_errors at runtime, it breaks Phoenix live reload + # due to protocol consolidation warnings + # :warnings_as_errors is deprecated via Code.compiler_options/2 since 1.18 + if Version.compare(System.version(), "1.18.0") == :lt do + Code.compiler_options(warnings_as_errors: false) end # Define workers and child supervisors to be supervised @@ -169,7 +154,8 @@ defmodule Pleroma.Application do limit: 500_000 ), build_cachex("rel_me", limit: 2500), - build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000) + build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000), + build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000) ] enddiff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex@@ -189,7 +189,40 @@ defmodule Pleroma.ApplicationRequirements do false end - if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do + language_detector_commands_status = + if Pleroma.Language.LanguageDetector.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by the currently enabled " <> + "language detection provider are not installed: " <> + inspect(Pleroma.Language.LanguageDetector.missing_dependencies()) + ) + + false + end + + translation_commands_status = + if Pleroma.Language.Translation.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by the currently enabled " <> + "translation provider are not installed: " <> + inspect(Pleroma.Language.Translation.missing_dependencies()) + ) + + false + end + + if Enum.all?( + [ + preview_proxy_commands_status, + language_detector_commands_status, + translation_commands_status | filter_commands_statuses + ], + & &1 + ) do :ok else {:error,diff --git a/lib/pleroma/backports.ex b/lib/pleroma/backports.ex@@ -0,0 +1,72 @@ +# Copyright 2012 Plataformatec +# Copyright 2021 The Elixir Team +# SPDX-License-Identifier: Apache-2.0 + +defmodule Pleroma.Backports do + import File, only: [dir?: 1] + + # <https://github.com/elixir-lang/elixir/pull/14242> + # To be removed when we require Elixir 1.19 + @doc """ + Tries to create the directory `path`. + + Missing parent directories are created. Returns `:ok` if successful, or + `{:error, reason}` if an error occurs. + + Typical error reasons are: + + * `:eacces` - missing search or write permissions for the parent + directories of `path` + * `:enospc` - there is no space left on the device + * `:enotdir` - a component of `path` is not a directory + + """ + @spec mkdir_p(Path.t()) :: :ok | {:error, File.posix() | :badarg} + def mkdir_p(path) do + do_mkdir_p(IO.chardata_to_string(path)) + end + + defp do_mkdir_p("/") do + :ok + end + + defp do_mkdir_p(path) do + parent = Path.dirname(path) + + if parent == path do + :ok + else + case do_mkdir_p(parent) do + :ok -> + case :file.make_dir(path) do + {:error, :eexist} -> + if dir?(path), do: :ok, else: {:error, :enotdir} + + other -> + other + end + + e -> + e + end + end + end + + @doc """ + Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure. + Otherwise `:ok`. + """ + @spec mkdir_p!(Path.t()) :: :ok + def mkdir_p!(path) do + case mkdir_p(path) do + :ok -> + :ok + + {:error, reason} -> + raise File.Error, + reason: reason, + action: "make directory (with -p)", + path: IO.chardata_to_string(path) + end + end +enddiff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex@@ -27,6 +27,7 @@ defmodule Pleroma.Config do Application.get_env(:pleroma, key, default) end + @impl true def get!(key) do value = get(key, nil)diff --git a/lib/pleroma/config/getting.ex b/lib/pleroma/config/getting.ex@@ -5,10 +5,13 @@ defmodule Pleroma.Config.Getting do @callback get(any()) :: any() @callback get(any(), any()) :: any() + @callback get!(any()) :: any() def get(key), do: get(key, nil) def get(key, default), do: impl().get(key, default) + def get!(key), do: impl().get!(key) + def impl do Application.get_env(:pleroma, :config_impl, Pleroma.Config) enddiff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex@@ -302,7 +302,7 @@ defmodule Pleroma.ConfigDB do end def to_elixir_types(%{"tuple" => entity}) do - Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1))) + Enum.reduce(entity, {}, &Tuple.insert_at(&2, tuple_size(&2), to_elixir_types(&1))) end def to_elixir_types(entity) when is_map(entity) dodiff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex@@ -20,7 +20,8 @@ defmodule Pleroma.Constants do "deleted_activity_id", "pleroma_internal", "generator", - "rules" + "rules", + "language" ] ) @@ -36,10 +37,12 @@ defmodule Pleroma.Constants do "updated", "emoji", "content", + "contentMap", "summary", "sensitive", "attachment", - "generator" + "generator", + "language" ] ) @@ -87,6 +90,7 @@ defmodule Pleroma.Constants do const(activity_types, do: [ + "Block", "Create", "Update", "Delete", @@ -96,10 +100,12 @@ defmodule Pleroma.Constants do "Add", "Remove", "Like", + "Dislike", "Announce", "Undo", "Flag", - "EmojiReact" + "EmojiReact", + "Listen" ] ) @@ -110,11 +116,16 @@ defmodule Pleroma.Constants do "Flag", "Follow", "Like", + "Dislike", "EmojiReact", "Announce" ] ) + const(object_types, + do: ~w[Event Question Answer Audio Video Image Article Note Page ChatMessage] + ) + # basic regex, just there to weed out potential mistakes # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 const(mime_regex,diff --git a/lib/pleroma/date_time.ex b/lib/pleroma/date_time.ex@@ -0,0 +1,3 @@ +defmodule Pleroma.DateTime do + @callback utc_now() :: NaiveDateTime.t() +enddiff --git a/lib/pleroma/date_time/impl.ex b/lib/pleroma/date_time/impl.ex@@ -0,0 +1,6 @@ +defmodule Pleroma.DateTime.Impl do + @behaviour Pleroma.DateTime + + @impl true + def utc_now, do: NaiveDateTime.utc_now() +enddiff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/content_language_map.ex@@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap do + use Ecto.Type + + import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode, + only: [good_locale_code?: 1] + + def type, do: :map + + def cast(%{} = object) do + with {status, %{} = data} when status in [:modified, :ok] <- validate_map(object) do + {:ok, data} + else + {_, nil} -> {:ok, nil} + {:error, _} -> :error + end + end + + def cast(_), do: :error + + def dump(data), do: {:ok, data} + + def load(data), do: {:ok, data} + + defp validate_map(%{} = object) do + {status, data} = + object + |> Enum.reduce({:ok, %{}}, fn + {lang, value}, {status, acc} when is_binary(lang) and is_binary(value) -> + if good_locale_code?(lang) do + {status, Map.put(acc, lang, value)} + else + {:modified, acc} + end + + _, {_status, acc} -> + {:modified, acc} + end) + + if data == %{} do + {status, nil} + else + {status, data} + end + end +enddiff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/language_code.ex@@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode do + use Ecto.Type + + def type, do: :string + + def cast(language) when is_binary(language) do + if good_locale_code?(language) do + {:ok, language} + else + {:error, :invalid_language} + end + end + + def cast(_), do: :error + + def dump(data), do: {:ok, data} + + def load(data), do: {:ok, data} + + def good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+\z$> + + def good_locale_code?(_code), do: false +enddiff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex@@ -24,12 +24,13 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji alias Pleroma.Emoji.Pack + alias Pleroma.SafeZip alias Pleroma.Utils @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do with :ok <- validate_not_empty([name]), - dir <- Path.join(emoji_path(), name), + dir <- path_join_name_safe(emoji_path(), name), :ok <- File.mkdir(dir) do save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")}) end @@ -65,43 +66,21 @@ defmodule Pleroma.Emoji.Pack do {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} def delete(name) do with :ok <- validate_not_empty([name]), - pack_path <- Path.join(emoji_path(), name) do + pack_path <- path_join_name_safe(emoji_path(), name) do File.rm_rf(pack_path) end end - @spec unpack_zip_emojies(list(tuple())) :: list(map()) - defp unpack_zip_emojies(zip_files) do - Enum.reduce(zip_files, [], fn - {_, path, s, _, _, _}, acc when elem(s, 2) == :regular -> - with( - filename <- Path.basename(path), - shortcode <- Path.basename(filename, Path.extname(filename)), - false <- Emoji.exist?(shortcode) - ) do - [%{path: path, filename: path, shortcode: shortcode} | acc] - else - _ -> acc - end - - _, acc -> - acc - end) - end - @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) :: {:ok, t()} | {:error, File.posix() | atom()} def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do - with {:ok, zip_files} <- :zip.table(to_charlist(file.path)), - [_ | _] = emojies <- unpack_zip_emojies(zip_files), + with {:ok, zip_files} <- SafeZip.list_dir_file(file.path), + [_ | _] = emojies <- map_zip_emojies(zip_files), {:ok, tmp_dir} <- Utils.tmp_dir("emoji") do try do {:ok, _emoji_files} = - :zip.unzip( - to_charlist(file.path), - [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}] - ) + SafeZip.unzip_file(file.path, tmp_dir, Enum.map(emojies, & &1[:path])) {_, updated_pack} = Enum.map_reduce(emojies, pack, fn item, emoji_pack -> @@ -246,6 +225,97 @@ defmodule Pleroma.Emoji.Pack do end end + def download_zip(name, opts \\ %{}) do + with :ok <- validate_not_empty([name]), + :ok <- validate_new_pack(name), + {:ok, archive_data} <- fetch_archive_data(opts), + pack_path <- path_join_name_safe(emoji_path(), name), + :ok <- create_pack_dir(pack_path), + :ok <- safe_unzip(archive_data, pack_path) do + ensure_pack_json(pack_path, archive_data, opts) + else + {:error, :empty_values} -> {:error, "Pack name cannot be empty"} + {:error, reason} when is_binary(reason) -> {:error, reason} + _ -> {:error, "Could not process pack"} + end + end + + defp create_pack_dir(pack_path) do + case File.mkdir_p(pack_path) do + :ok -> :ok + {:error, _} -> {:error, "Could not create the pack directory"} + end + end + + defp safe_unzip(archive_data, pack_path) do + case SafeZip.unzip_data(archive_data, pack_path) do + {:ok, _} -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + _ -> {:error, "Could not unzip pack"} + end + end + + defp validate_new_pack(name) do + pack_path = path_join_name_safe(emoji_path(), name) + + if File.exists?(pack_path) do + {:error, "Pack already exists, refusing to import #{name}"} + else + :ok + end + end + + defp fetch_archive_data(%{url: url}) do + case Pleroma.HTTP.get(url) do + {:ok, %{status: 200, body: data}} -> {:ok, data} + _ -> {:error, "Could not download pack"} + end + end + + defp fetch_archive_data(%{file: %Plug.Upload{path: path}}) do + case File.read(path) do + {:ok, data} -> {:ok, data} + _ -> {:error, "Could not read the uploaded pack file"} + end + end + + defp fetch_archive_data(_) do + {:error, "Neither file nor URL was present in the request"} + end + + defp ensure_pack_json(pack_path, archive_data, opts) do + pack_json_path = Path.join(pack_path, "pack.json") + + if not File.exists?(pack_json_path) do + create_pack_json(pack_path, pack_json_path, archive_data, opts) + end + + :ok + end + + defp create_pack_json(pack_path, pack_json_path, archive_data, opts) do + emoji_map = + Pleroma.Emoji.Loader.make_shortcode_to_file_map( + pack_path, + Map.get(opts, :exts, [".png", ".gif", ".jpg"]) + ) + + archive_sha = :crypto.hash(:sha256, archive_data) |> Base.encode16() + + pack_json = %{ + pack: %{ + license: Map.get(opts, :license, ""), + homepage: Map.get(opts, :homepage, ""), + description: Map.get(opts, :description, ""), + src: Map.get(opts, :url), + src_sha256: archive_sha + }, + files: emoji_map + } + + File.write!(pack_json_path, Jason.encode!(pack_json, pretty: true)) + end + @spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()} def download(name, url, as) do uri = url |> String.trim() |> URI.parse() @@ -292,7 +362,7 @@ defmodule Pleroma.Emoji.Pack do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do name = Path.basename(name) - pack_file = Path.join([emoji_path(), name, "pack.json"]) + pack_file = path_join_name_safe(emoji_path(), name) |> Path.join("pack.json") with {:ok, _} <- File.stat(pack_file), {:ok, pack_data} <- File.read(pack_file) do @@ -416,10 +486,9 @@ defmodule Pleroma.Emoji.Pack do end defp create_archive_and_cache(pack, hash) do - files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] - - {:ok, {_, result}} = - :zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)]) + pack_file_list = Enum.into(pack.files, [], fn {_, f} -> f end) + files = ["pack.json" | pack_file_list] + {:ok, {_, result}} = SafeZip.zip("#{pack.name}.zip", files, pack.path, true) ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files)) @@ -478,7 +547,7 @@ defmodule Pleroma.Emoji.Pack do end defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do - file_path = Path.join(pack.path, filename) + file_path = path_join_safe(pack.path, filename) create_subdirs(file_path) with {:ok, _} <- File.copy(upload_path, file_path) do @@ -497,8 +566,8 @@ defmodule Pleroma.Emoji.Pack do end defp rename_file(pack, filename, new_filename) do - old_path = Path.join(pack.path, filename) - new_path = Path.join(pack.path, new_filename) + old_path = path_join_safe(pack.path, filename) + new_path = path_join_safe(pack.path, new_filename) create_subdirs(new_path) with :ok <- File.rename(old_path, new_path) do @@ -510,13 +579,13 @@ defmodule Pleroma.Emoji.Pack do with true <- String.contains?(file_path, "/"), path <- Path.dirname(file_path), false <- File.exists?(path) do - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) end end defp remove_file(pack, shortcode) do with {:ok, filename} <- get_filename(pack, shortcode), - emoji <- Path.join(pack.path, filename), + emoji <- path_join_safe(pack.path, filename), :ok <- File.rm(emoji) do remove_dir_if_empty(emoji, filename) end @@ -534,7 +603,7 @@ defmodule Pleroma.Emoji.Pack do defp get_filename(pack, shortcode) do with %{^shortcode => filename} when is_binary(filename) <- pack.files, - file_path <- Path.join(pack.path, filename), + file_path <- path_join_safe(pack.path, filename), {:ok, _} <- File.stat(file_path) do {:ok, filename} else @@ -558,7 +627,7 @@ defmodule Pleroma.Emoji.Pack do emoji_path = emoji_path() # Create the directory first if it does not exist. This is probably the first request made # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + with {:create_dir, :ok} <- {:create_dir, Pleroma.Backports.mkdir_p(emoji_path)}, {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do {:ok, Enum.sort(results)} else @@ -583,12 +652,11 @@ defmodule Pleroma.Emoji.Pack do end defp unzip(archive, pack_info, remote_pack, local_pack) do - with :ok <- File.mkdir_p!(local_pack.path) do - files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + with :ok <- Pleroma.Backports.mkdir_p!(local_pack.path) do + files = Enum.map(remote_pack["files"], fn {_, path} -> path end) # Fallback cannot contain a pack.json file - files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files] - - :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) + files = if pack_info[:fallback], do: files, else: ["pack.json" | files] + SafeZip.unzip_data(archive, local_pack.path, files) end end @@ -649,13 +717,43 @@ defmodule Pleroma.Emoji.Pack do end defp validate_has_all_files(pack, zip) do - with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do - # Check if all files from the pack.json are in the archive - pack.files - |> Enum.all?(fn {_, from_manifest} -> - List.keyfind(f_list, to_charlist(from_manifest), 0) + # Check if all files from the pack.json are in the archive + eset = + Enum.reduce(pack.files, MapSet.new(), fn + {_, file}, s -> MapSet.put(s, to_charlist(file)) end) - |> if(do: :ok, else: {:error, :incomplete}) + + if SafeZip.contains_all_data?(zip, eset), + do: :ok, + else: {:error, :incomplete} + end + + defp path_join_name_safe(dir, name) do + if to_string(name) != Path.basename(name) or name in ["..", ".", ""] do + raise "Invalid or malicious pack name: #{name}" + else + Path.join(dir, name) end end + + defp path_join_safe(dir, path) do + {:ok, safe_path} = Path.safe_relative(path) + Path.join(dir, safe_path) + end + + defp map_zip_emojies(zip_files) do + Enum.reduce(zip_files, [], fn path, acc -> + with( + filename <- Path.basename(path), + shortcode <- Path.basename(filename, Path.extname(filename)), + # note: this only checks the shortcode, if an emoji already exists on the same path, but + # with a different shortcode, the existing one will be degraded to an alias of the new + false <- Emoji.exist?(shortcode) + ) do + [%{path: path, filename: path, shortcode: shortcode} | acc] + else + _ -> acc + end + end) + end enddiff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex@@ -65,21 +65,12 @@ defmodule Pleroma.Frontend do end def unzip(zip, dest) do - with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do - File.rm_rf!(dest) - File.mkdir_p!(dest) - - Enum.each(unzipped, fn {filename, data} -> - path = filename - - new_file_path = Path.join(dest, path) - - new_file_path - |> Path.dirname() - |> File.mkdir_p!() + File.rm_rf!(dest) + Pleroma.Backports.mkdir_p!(dest) - File.write!(new_file_path, data) - end) + case Pleroma.SafeZip.unzip_data(zip, dest) do + {:ok, _} -> :ok + error -> error end end @@ -99,7 +90,7 @@ defmodule Pleroma.Frontend do defp install_frontend(frontend_info, source, dest) do from = frontend_info["build_dir"] || "dist" File.rm_rf!(dest) - File.mkdir_p!(dest) + Pleroma.Backports.mkdir_p!(dest) File.cp_r!(Path.join([source, from]), dest) :ok enddiff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex@@ -22,14 +22,18 @@ defmodule Pleroma.Gopher.Server do def init([ip, port]) do Logger.info("Starting gopher server on #{port}") - :ranch.start_listener( - :gopher, - 100, - :ranch_tcp, - [ip: ip, port: port], - __MODULE__.ProtocolHandler, - [] - ) + {:ok, _pid} = + :ranch.start_listener( + :gopher, + :ranch_tcp, + %{ + num_acceptors: 100, + max_connections: 100, + socket_opts: [ip: ip, port: port] + }, + __MODULE__.ProtocolHandler, + [] + ) {:ok, %{ip: ip, port: port}} end @@ -43,13 +47,13 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility - def start_link(ref, socket, transport, opts) do - pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) + def start_link(ref, transport, opts) do + pid = spawn_link(__MODULE__, :init, [ref, transport, opts]) {:ok, pid} end - def init(ref, socket, transport, [] = _Opts) do - :ok = :ranch.accept_ack(ref) + def init(ref, transport, opts \\ []) do + {:ok, socket} = :ranch.handshake(ref, opts) loop(socket, transport) enddiff --git a/lib/pleroma/hashtag.ex b/lib/pleroma/hashtag.ex@@ -12,6 +12,7 @@ defmodule Pleroma.Hashtag do alias Pleroma.Hashtag alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.User.HashtagFollow schema "hashtags" do field(:name, :string) @@ -27,6 +28,14 @@ defmodule Pleroma.Hashtag do |> String.trim() end + def get_by_id(id) do + Repo.get(Hashtag, id) + end + + def get_by_name(name) do + Repo.get_by(Hashtag, name: normalize_name(name)) + end + def get_or_create_by_name(name) do changeset = changeset(%Hashtag{}, %{name: name}) @@ -103,4 +112,84 @@ defmodule Pleroma.Hashtag do {:ok, deleted_count} end end + + def get_followers(%Hashtag{id: hashtag_id}) do + from(hf in HashtagFollow) + |> where([hf], hf.hashtag_id == ^hashtag_id) + |> join(:inner, [hf], u in assoc(hf, :user)) + |> select([hf, u], u.id) + |> Repo.all() + end + + def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}}) + when is_list(tags) do + tags + |> Enum.map(&get_followers/1) + |> List.flatten() + |> Enum.uniq() + end + + def get_recipients_for_activity(_activity), do: [] + + def search(query, options \\ []) do + limit = Keyword.get(options, :limit, 20) + offset = Keyword.get(options, :offset, 0) + + search_terms = + query + |> String.downcase() + |> String.trim() + |> String.split(~r/\s+/) + |> Enum.filter(&(&1 != "")) + |> Enum.map(&String.trim_leading(&1, "#")) + |> Enum.filter(&(&1 != "")) + + if Enum.empty?(search_terms) do + [] + else + # Use PostgreSQL's ANY operator with array for efficient multi-term search + # This is much more efficient than multiple OR clauses + search_patterns = Enum.map(search_terms, &"%#{&1}%") + + # Create ranking query that prioritizes exact matches and closer matches + # Use a subquery to properly handle computed columns in ORDER BY + base_query = + from(ht in Hashtag, + where: fragment("LOWER(?) LIKE ANY(?)", ht.name, ^search_patterns), + select: %{ + name: ht.name, + # Ranking: exact matches get highest priority (0) + # then prefix matches (1), then contains (2) + match_rank: + fragment( + """ + CASE + WHEN LOWER(?) = ANY(?) THEN 0 + WHEN LOWER(?) LIKE ANY(?) THEN 1 + ELSE 2 + END + """, + ht.name, + ^search_terms, + ht.name, + ^Enum.map(search_terms, &"#{&1}%") + ), + # Secondary sort by name length (shorter names first) + name_length: fragment("LENGTH(?)", ht.name) + } + ) + + from(result in subquery(base_query), + order_by: [ + asc: result.match_rank, + asc: result.name_length, + asc: result.name + ], + limit: ^limit, + offset: ^offset + ) + |> Repo.all() + |> Enum.map(& &1.name) + end + end enddiff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex@@ -105,20 +105,57 @@ defmodule Pleroma.HTTP do end defp adapter_middlewares(Tesla.Adapter.Gun, extra_middleware) do - [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool] ++ + default_middleware() ++ + [Pleroma.Tesla.Middleware.ConnectionPool] ++ extra_middleware end - defp adapter_middlewares({Tesla.Adapter.Finch, _}, extra_middleware) do - [Tesla.Middleware.FollowRedirects] ++ extra_middleware - end - defp adapter_middlewares(_, extra_middleware) do - if Pleroma.Config.get(:env) == :test do - # Emulate redirects in test env, which are handled by adapters in other environments - [Tesla.Middleware.FollowRedirects] - else - extra_middleware + # A lot of tests are written expecting unencoded URLs + # and the burden of fixing that is high. Also it makes + # them hard to read. Tests will opt-in when we want to validate + # the encoding is being done correctly. + cond do + Pleroma.Config.get(:env) == :test and Pleroma.Config.get(:test_url_encoding) -> + default_middleware() + + Pleroma.Config.get(:env) == :test -> + # Emulate redirects in test env, which are handled by adapters in other environments + [Tesla.Middleware.FollowRedirects] + + # Hackney and Finch + true -> + default_middleware() ++ extra_middleware end end + + defp default_middleware, + do: [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.EncodeUrl] + + def encode_url(url) when is_binary(url) do + URI.parse(url) + |> then(fn parsed -> + path = encode_path(parsed.path) + query = encode_query(parsed.query) + + %{parsed | path: path, query: query} + end) + |> URI.to_string() + end + + defp encode_path(nil), do: nil + + defp encode_path(path) when is_binary(path) do + path + |> URI.decode() + |> URI.encode() + end + + defp encode_query(nil), do: nil + + defp encode_query(query) when is_binary(query) do + query + |> URI.decode_query() + |> URI.encode_query() + end enddiff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex@@ -15,25 +15,7 @@ defmodule Pleroma.Instances do defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: Instance - defdelegate get_consistently_unreachable, to: Instance - - def set_consistently_unreachable(url_or_host), - do: set_unreachable(url_or_host, reachability_datetime_threshold()) - - def reachability_datetime_threshold do - federation_reachability_timeout_days = - Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0) - - if federation_reachability_timeout_days > 0 do - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -federation_reachability_timeout_days * 24 * 3600, - :second - ) - else - ~N[0000-01-01 00:00:00] - end - end + defdelegate get_unreachable, to: Instance def host(url_or_host) when is_binary(url_or_host) do if url_or_host =~ ~r/^http/i do @@ -42,4 +24,21 @@ defmodule Pleroma.Instances do url_or_host end end + + @doc "Schedules reachability checks for all unreachable instances" + def check_all_unreachable do + get_unreachable() + |> Enum.each(fn {domain, _} -> + Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) + |> Oban.insert() + end) + end + + @doc "Deletes all users and activities for unreachable instances" + def delete_all_unreachable do + get_unreachable() + |> Enum.each(fn {domain, _} -> + Instance.delete(domain) + end) + end enddiff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex@@ -9,7 +9,6 @@ defmodule Pleroma.Instances.Instance do alias Pleroma.Instances.Instance alias Pleroma.Maps alias Pleroma.Repo - alias Pleroma.User alias Pleroma.Workers.DeleteWorker use Ecto.Schema @@ -51,7 +50,7 @@ defmodule Pleroma.Instances.Instance do |> cast(params, [:software_name, :software_version, :software_repository]) end - def filter_reachable([]), do: %{} + def filter_reachable([]), do: [] def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do hosts = @@ -68,19 +67,15 @@ defmodule Pleroma.Instances.Instance do ) |> Map.new(& &1) - reachability_datetime_threshold = Instances.reachability_datetime_threshold() - for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do host = host(entry) unreachable_since = unreachable_since_by_host[host] - if !unreachable_since || - NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do - {entry, unreachable_since} + if is_nil(unreachable_since) do + entry end end |> Enum.filter(& &1) - |> Map.new(& &1) end def reachable?(url_or_host) when is_binary(url_or_host) do @@ -88,7 +83,7 @@ defmodule Pleroma.Instances.Instance do from(i in Instance, where: i.host == ^host(url_or_host) and - i.unreachable_since <= ^Instances.reachability_datetime_threshold(), + not is_nil(i.unreachable_since), select: true ) ) @@ -97,9 +92,16 @@ defmodule Pleroma.Instances.Instance do def reachable?(url_or_host) when is_binary(url_or_host), do: true def set_reachable(url_or_host) when is_binary(url_or_host) do - %Instance{host: host(url_or_host)} - |> changeset(%{unreachable_since: nil}) - |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host) + host = host(url_or_host) + + result = + %Instance{host: host} + |> changeset(%{unreachable_since: nil}) + |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host) + + Pleroma.Workers.ReachabilityWorker.delete_jobs_for_host(host) + + result end def set_reachable(_), do: {:error, nil} @@ -132,11 +134,9 @@ defmodule Pleroma.Instances.Instance do def set_unreachable(_, _), do: {:error, nil} - def get_consistently_unreachable do - reachability_datetime_threshold = Instances.reachability_datetime_threshold() - + def get_unreachable do from(i in Instance, - where: ^reachability_datetime_threshold > i.unreachable_since, + where: not is_nil(i.unreachable_since), order_by: i.unreachable_since, select: {i.host, i.unreachable_since} ) @@ -296,20 +296,14 @@ defmodule Pleroma.Instances.Instance do Deletes all users from an instance in a background task, thus also deleting all of those users' activities and notifications. """ - def delete_users_and_activities(host) when is_binary(host) do + def delete(host) when is_binary(host) do DeleteWorker.new(%{"op" => "delete_instance", "host" => host}) |> Oban.insert() end - def perform(:delete_instance, host) when is_binary(host) do - User.Query.build(%{nickname: "@#{host}"}) - |> Repo.chunk_stream(100, :batches) - |> Stream.each(fn users -> - users - |> Enum.each(fn user -> - User.perform(:delete, user) - end) - end) - |> Stream.run() + @doc "Schedules reachability check for instance" + def check_unreachable(domain) when is_binary(domain) do + Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) + |> Oban.insert() end enddiff --git a/lib/pleroma/language/language_detector.ex b/lib/pleroma/language/language_detector.ex@@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector do + import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode, + only: [good_locale_code?: 1] + + @words_threshold 4 + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + + def configured? do + provider = get_provider() + + !!provider and provider.configured?() + end + + def missing_dependencies do + provider = get_provider() + + if provider do + provider.missing_dependencies() + else + [] + end + end + + # Strip tags from text, etc. + defp prepare_text(text) do + text + |> Floki.parse_fragment!() + |> Floki.filter_out( + ".h-card, .mention, .hashtag, .u-url, .quote-inline, .recipients-inline, code, pre" + ) + |> Floki.text() + end + + def detect(text) do + provider = get_provider() + + text = prepare_text(text) + word_count = text |> String.split(~r/\s+/) |> Enum.count() + + if word_count < @words_threshold or !provider or !provider.configured?() do + nil + else + with language <- provider.detect(text), + true <- good_locale_code?(language) do + language + else + _ -> nil + end + end + end + + defp get_provider do + @config_impl.get([__MODULE__, :provider]) + end +enddiff --git a/lib/pleroma/language/language_detector/fasttext.ex b/lib/pleroma/language/language_detector/fasttext.ex@@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector.Fasttext do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.LanguageDetector.Provider + + @behaviour Provider + + @impl Provider + def missing_dependencies do + if Pleroma.Utils.command_available?("fasttext") do + [] + else + ["fasttext"] + end + end + + @impl Provider + def configured?, do: not_empty_string(get_model()) + + @impl Provider + def detect(text) do + text_path = Path.join(System.tmp_dir!(), "fasttext-#{Ecto.UUID.generate()}") + + File.write(text_path, text |> String.replace(~r/\s+/, " ")) + + detected_language = + case System.cmd("fasttext", ["predict", get_model(), text_path]) do + {"__label__" <> language, _} -> + language |> String.trim() + + _ -> + nil + end + + File.rm(text_path) + + detected_language + end + + defp get_model do + Pleroma.Config.get([__MODULE__, :model]) + end +enddiff --git a/lib/pleroma/language/language_detector/provider.ex b/lib/pleroma/language/language_detector/provider.ex@@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector.Provider do + @callback missing_dependencies() :: [String.t()] + + @callback configured?() :: boolean() + + @callback detect(text :: String.t()) :: String.t() | nil +enddiff --git a/lib/pleroma/language/translation.ex b/lib/pleroma/language/translation.ex@@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation do + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + def configured? do + provider = get_provider() + + !!provider and provider.configured?() + end + + def missing_dependencies do + provider = get_provider() + + if provider do + provider.missing_dependencies() + else + [] + end + end + + def translate(text, source_language, target_language) do + cache_key = get_cache_key(text, source_language, target_language) + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + provider = get_provider() + + result = + if !configured?() do + {:error, :not_found} + else + provider.translate(text, source_language, target_language) + |> scrub_html() + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + def supported_languages(type) when type in [:source, :target] do + provider = get_provider() + + cache_key = "#{type}_languages/#{provider.name()}" + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + result = + if !configured?() do + {:error, :not_found} + else + provider.supported_languages(type) + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + def languages_matrix do + provider = get_provider() + + cache_key = "languages_matrix/#{provider.name()}" + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + result = + if !configured?() do + {:error, :not_found} + else + provider.languages_matrix() + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp get_provider, do: Pleroma.Config.get([__MODULE__, :provider]) + + defp get_cache_key(text, source_language, target_language) do + "#{source_language}/#{target_language}/#{content_hash(text)}" + end + + defp store_result({:ok, result}, cache_key) do + @cachex.put(:translations_cache, cache_key, result) + end + + defp store_result(_, _), do: nil + + defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64() + + defp scrub_html({:ok, %{content: content} = result}) when is_binary(content) do + scrubbers = Pleroma.Config.get([:markup, :scrub_policy]) + + content + |> Pleroma.HTML.filter_tags(scrubbers) + + {:ok, %{result | content: content}} + end + + defp scrub_html(result), do: result +enddiff --git a/lib/pleroma/language/translation/deepl.ex b/lib/pleroma/language/translation/deepl.ex@@ -0,0 +1,121 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Deepl do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "DeepL" + + @impl Provider + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) + + @impl Provider + def translate(content, source_language, target_language) do + endpoint = + base_url() + |> URI.merge("/v2/translate") + |> URI.to_string() + + case Pleroma.HTTP.post( + endpoint, + Jason.encode!(%{ + text: [content], + source_lang: source_language |> String.upcase(), + target_lang: target_language, + tag_handling: "html" + }), + [ + {"Content-Type", "application/json"}, + {"Authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 456}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translations" => [ + %{"text" => content, "detected_source_language" => detected_source_language} + ] + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: detected_source_language, + provider: @name + }} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def supported_languages(type) when type in [:source, :target] do + endpoint = + base_url() + |> URI.merge("/v2/languages") + |> URI.to_string() + + case Pleroma.HTTP.post( + endpoint <> "?" <> URI.encode_query(%{type: type}), + "", + [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"language" => language} -> language |> String.downcase() end) + |> Enum.map(fn language -> + if String.contains?(language, "-") do + [language, language |> String.split("-") |> Enum.at(0)] + else + language + end + end) + |> List.flatten() + |> Enum.uniq() + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def languages_matrix do + with {:ok, source_languages} <- supported_languages(:source), + {:ok, target_languages} <- supported_languages(:target) do + {:ok, + Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)} + else + {:error, error} -> {:error, error} + end + end + + @impl Provider + def name, do: @name + + defp base_url do + Pleroma.Config.get([__MODULE__, :base_url]) + end + + defp api_key do + Pleroma.Config.get([__MODULE__, :api_key]) + end +enddiff --git a/lib/pleroma/language/translation/libretranslate.ex b/lib/pleroma/language/translation/libretranslate.ex@@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Libretranslate do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "LibreTranslate" + + @impl Provider + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) + + @impl Provider + def translate(content, source_language, target_language) do + case Pleroma.HTTP.post( + base_url() <> "/translate", + Jason.encode!(%{ + q: content, + source: source_language |> String.upcase(), + target: target_language, + format: "html", + api_key: api_key() + }), + [ + {"Content-Type", "application/json"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 403}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translatedText" => content + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: source_language, + provider: @name + }} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def supported_languages(_) do + case Pleroma.HTTP.get(base_url() <> "/languages") do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"code" => code} -> code end) + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def languages_matrix do + with {:ok, source_languages} <- supported_languages(:source), + {:ok, target_languages} <- supported_languages(:target) do + {:ok, + Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)} + else + {:error, error} -> {:error, error} + end + end + + @impl Provider + def name, do: @name + + defp base_url do + Pleroma.Config.get([__MODULE__, :base_url]) + end + + defp api_key do + Pleroma.Config.get([__MODULE__, :api_key], "") + end +enddiff --git a/lib/pleroma/language/translation/provider.ex b/lib/pleroma/language/translation/provider.ex@@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Provider do + alias Pleroma.Language.Translation.Provider + + @callback missing_dependencies() :: [String.t()] + + @callback configured?() :: boolean() + + @callback translate( + content :: String.t(), + source_language :: String.t(), + target_language :: String.t() + ) :: + {:ok, + %{ + content: String.t(), + detected_source_language: String.t(), + provider: String.t() + }} + | {:error, atom()} + + @callback supported_languages(type :: :string | :target) :: + {:ok, [String.t()]} | {:error, atom()} + + @callback languages_matrix() :: {:ok, map()} | {:error, atom()} + + @callback name() :: String.t() + + defmacro __using__(_opts) do + quote do + @impl Provider + def missing_dependencies, do: [] + + defoverridable missing_dependencies: 0 + end + end +enddiff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex@@ -15,6 +15,14 @@ defmodule Pleroma.LDAP do GenServer.start_link(__MODULE__, [], name: __MODULE__) end + def bind_user(name, password) do + GenServer.call(__MODULE__, {:bind_user, name, password}) + end + + def change_password(name, password, new_password) do + GenServer.call(__MODULE__, {:change_password, name, password, new_password}) + end + @impl true def init(state) do case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do @@ -47,33 +55,16 @@ defmodule Pleroma.LDAP do def handle_info(:connect, _state), do: do_handle_connect() def handle_info({:bind_after_reconnect, name, password, from}, state) do - result = bind_user(state[:handle], name, password) + result = do_bind_user(state[:handle], name, password) GenServer.reply(from, result) {:noreply, state} end - defp do_handle_connect do - state = - case connect() do - {:ok, handle} -> - :eldap.controlling_process(handle, self()) - Process.link(handle) - [handle: handle] - - _ -> - Logger.error("Failed to connect to LDAP. Retrying in 5000ms") - Process.send_after(self(), :connect, 5_000) - [] - end - - {:noreply, state} - end - @impl true def handle_call({:bind_user, name, password}, from, state) do - case bind_user(state[:handle], name, password) do + case do_bind_user(state[:handle], name, password) do :needs_reconnect -> Process.send(self(), {:bind_after_reconnect, name, password, from}, []) {:noreply, state, {:continue, :connect}} @@ -83,6 +74,12 @@ defmodule Pleroma.LDAP do end end + def handle_call({:change_password, name, password, new_password}, _from, state) do + result = change_password(state[:handle], name, password, new_password) + + {:reply, result, state, :hibernate} + end + @impl true def terminate(_, state) do handle = Keyword.get(state, :handle) @@ -94,6 +91,23 @@ defmodule Pleroma.LDAP do :ok end + defp do_handle_connect do + state = + case connect() do + {:ok, handle} -> + :eldap.controlling_process(handle, self()) + Process.link(handle) + [handle: handle] + + _ -> + Logger.error("Failed to connect to LDAP. Retrying in 5000ms") + Process.send_after(self(), :connect, 5_000) + [] + end + + {:noreply, state} + end + defp connect do ldap = Config.get(:ldap, []) host = Keyword.get(ldap, :host, "localhost") @@ -157,18 +171,17 @@ defmodule Pleroma.LDAP do end end - defp bind_user(handle, name, password) do - uid = Config.get([:ldap, :uid], "cn") - base = Config.get([:ldap, :base]) + defp do_bind_user(handle, name, password) do + dn = make_dn(name) - case :eldap.simple_bind(handle, "#{uid}=#{name},#{base}", password) do + case :eldap.simple_bind(handle, dn, password) do :ok -> case fetch_user(name) do %User{} = user -> user _ -> - register_user(handle, base, uid, name) + register_user(handle, ldap_base(), ldap_uid(), name) end # eldap does not inform us of socket closure @@ -227,6 +240,14 @@ defmodule Pleroma.LDAP do end end + defp change_password(handle, name, password, new_password) do + dn = make_dn(name) + + with :ok <- :eldap.simple_bind(handle, dn, password) do + :eldap.modify_password(handle, dn, to_charlist(new_password), to_charlist(password)) + end + end + defp decode_certfile(file) do with {:ok, data} <- File.read(file) do data @@ -238,4 +259,13 @@ defmodule Pleroma.LDAP do [] end end + + defp ldap_uid, do: to_charlist(Config.get([:ldap, :uid], "cn")) + defp ldap_base, do: to_charlist(Config.get([:ldap, :base])) + + defp make_dn(name) do + uid = ldap_uid() + base = ldap_base() + ~c"#{uid}=#{name},#{base}" + end enddiff --git a/lib/pleroma/mogrify_behaviour.ex b/lib/pleroma/mogrify_behaviour.ex@@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MogrifyBehaviour do + @moduledoc """ + Behaviour for Mogrify operations. + This module defines the interface for Mogrify operations that can be mocked in tests. + """ + + @callback open(binary()) :: map() + @callback custom(map(), binary()) :: map() + @callback custom(map(), binary(), binary()) :: map() + @callback save(map(), keyword()) :: map() +enddiff --git a/lib/pleroma/mogrify_wrapper.ex b/lib/pleroma/mogrify_wrapper.ex@@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MogrifyWrapper do + @moduledoc """ + Default implementation of MogrifyBehaviour that delegates to Mogrify. + """ + @behaviour Pleroma.MogrifyBehaviour + + @impl true + def open(file) do + Mogrify.open(file) + end + + @impl true + def custom(image, action) do + Mogrify.custom(image, action) + end + + @impl true + def custom(image, action, options) do + Mogrify.custom(image, action, options) + end + + @impl true + def save(image, opts) do + Mogrify.save(image, opts) + end +enddiff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex@@ -99,27 +99,6 @@ defmodule Pleroma.Object do def get_by_id(nil), do: nil def get_by_id(id), do: Repo.get(Object, id) - @spec get_by_id_and_maybe_refetch(integer(), list()) :: Object.t() | nil - def get_by_id_and_maybe_refetch(id, opts \\ []) do - with %Object{updated_at: updated_at} = object <- get_by_id(id) do - if opts[:interval] && - NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do - case Fetcher.refetch_object(object) do - {:ok, %Object{} = object} -> - object - - e -> - Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}") - object - end - else - object - end - else - nil -> nil - end - end - def get_by_ap_id(nil), do: nil def get_by_ap_id(ap_id) dodiff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex@@ -48,6 +48,19 @@ defmodule Pleroma.Object.Containment do defp compare_uris(_id_uri, _other_uri), do: :error @doc """ + Checks whether an URL to fetch from is from the local server. + + We never want to fetch from ourselves; if it's not in the database + it can't be authentic and must be a counterfeit. + """ + def contain_local_fetch(id) do + case compare_uris(URI.parse(id), Pleroma.Web.Endpoint.struct_url()) do + :ok -> :error + _ -> :ok + end + end + + @doc """ Checks that an imported AP object's actor matches the host it came from. """ def contain_origin(_id, %{"actor" => nil}), do: :errordiff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex@@ -4,7 +4,6 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.HTTP - alias Pleroma.Instances alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment @@ -145,21 +144,25 @@ defmodule Pleroma.Object.Fetcher do Logger.debug("Fetching object #{id} via AP") with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, + {_, true} <- {:mrf, MRF.id_filter(id)}, + {_, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, {:ok, body} <- get_object(id), {:ok, data} <- safe_json_decode(body), :ok <- Containment.contain_origin_from_id(id, data) do - if not Instances.reachable?(id) do - Instances.set_reachable(id) - end - {:ok, data} else {:scheme, _} -> {:error, "Unsupported URI scheme"} + {:local_fetch, _} -> + {:error, "Trying to fetch local resource"} + {:error, e} -> {:error, e} + {:mrf, false} -> + {:error, {:reject, "Filtered by id"}} + e -> {:error, e} end @@ -168,6 +171,14 @@ defmodule Pleroma.Object.Fetcher do def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"} + defp check_crossdomain_redirect(final_host, _original_url) when is_nil(final_host) do + {:cross_domain_redirect, false} + end + + defp check_crossdomain_redirect(final_host, original_url) do + {:cross_domain_redirect, final_host != URI.parse(original_url).host} + end + defp get_object(id) do date = Pleroma.Signature.signed_date() @@ -177,19 +188,29 @@ defmodule Pleroma.Object.Fetcher do |> sign_fetch(id, date) case HTTP.get(id, headers) do + {:ok, %{body: body, status: code, headers: headers, url: final_url}} + when code in 200..299 -> + remote_host = if final_url, do: URI.parse(final_url).host, else: nil + + with {:cross_domain_redirect, false} <- check_crossdomain_redirect(remote_host, id), + {_, content_type} <- List.keyfind(headers, "content-type", 0), + {:ok, _media_type} <- verify_content_type(content_type) do + {:ok, body} + else + {:cross_domain_redirect, true} -> + {:error, {:cross_domain_redirect, true}} + + error -> + error + end + + # Handle the case where URL is not in the response (older HTTP library versions) {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 -> case List.keyfind(headers, "content-type", 0) do {_, content_type} -> - case Plug.Conn.Utils.media_type(content_type) do - {:ok, "application", "activity+json", _} -> - {:ok, body} - - {:ok, "application", "ld+json", - %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> - {:ok, body} - - _ -> - {:error, {:content_type, content_type}} + case verify_content_type(content_type) do + {:ok, _} -> {:ok, body} + error -> error end _ -> @@ -212,4 +233,17 @@ defmodule Pleroma.Object.Fetcher do defp safe_json_decode(nil), do: {:ok, nil} defp safe_json_decode(json), do: Jason.decode(json) + + defp verify_content_type(content_type) do + case Plug.Conn.Utils.media_type(content_type) do + {:ok, "application", "activity+json", _} -> + {:ok, :activity_json} + + {:ok, "application", "ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> + {:ok, :ld_json} + + _ -> + {:error, {:content_type, content_type}} + end + end enddiff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex@@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersion do - @spec version() :: String.t() | nil - def version do - # OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version - [ - Path.join(:code.root_dir(), "OTP_VERSION"), - Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"]) - ] - |> get_version_from_files() - end - - @spec get_version_from_files([Path.t()]) :: String.t() | nil - def get_version_from_files([]), do: nil - - def get_version_from_files([path | paths]) do - if File.exists?(path) do - path - |> File.read!() - |> String.replace(~r/\r|\n|\s/, "") - else - get_version_from_files(paths) - end - end -enddiff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex@@ -89,9 +89,9 @@ defmodule Pleroma.Pagination do defp cast_params(params) do param_types = %{ - min_id: :string, - since_id: :string, - max_id: :string, + min_id: params[:id_type] || :string, + since_id: params[:id_type] || :string, + max_id: params[:id_type] || :string, offset: :integer, limit: :integer, skip_extra_order: :boolean,diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex@@ -16,17 +16,24 @@ defmodule Pleroma.ReleaseTasks do end end + def find_module(task) do + module_name = + task + |> String.split(".") + |> Enum.map(&String.capitalize/1) + |> then(fn x -> [Mix, Tasks, Pleroma] ++ x end) + |> Module.concat() + + case Code.ensure_loaded(module_name) do + {:module, _} -> module_name + _ -> nil + end + end + defp mix_task(task, args) do Application.load(:pleroma) - {:ok, modules} = :application.get_key(:pleroma, :modules) - - module = - Enum.find(modules, fn module -> - module = Module.split(module) - match?(["Mix", "Tasks", "Pleroma" | _], module) and - String.downcase(List.last(module)) == task - end) + module = find_module(task) if module do module.run(args)diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex@@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do @failed_request_ttl :timer.seconds(60) @methods ~w(GET HEAD) + @allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], []) + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) def max_read_duration_default, do: @max_read_duration @@ -156,6 +158,8 @@ defmodule Pleroma.ReverseProxy do Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") method = method |> String.downcase() |> String.to_existing_atom() + url = maybe_encode_url(url) + case client().request(method, url, headers, "", opts) do {:ok, code, headers, client} when code in @valid_resp_codes -> {:ok, code, downcase_headers(headers), client} @@ -301,10 +305,26 @@ defmodule Pleroma.ReverseProxy do headers |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> build_resp_cache_headers(opts) + |> sanitise_content_type() |> build_resp_content_disposition_header(opts) |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) end + defp sanitise_content_type(headers) do + original_ct = get_content_type(headers) + + safe_ct = + Pleroma.Web.Plugs.Utils.get_safe_mime_type( + %{allowed_mime_types: @allowed_mime_types}, + original_ct + ) + + [ + {"content-type", safe_ct} + | Enum.filter(headers, fn {k, _v} -> k != "content-type" end) + ] + end + defp build_resp_cache_headers(headers, _opts) do has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) @@ -431,4 +451,18 @@ defmodule Pleroma.ReverseProxy do _ -> delete_resp_header(conn, "content-length") end end + + # Only when Tesla adapter is Hackney or Finch does the URL + # need encoding before Reverse Proxying as both end up + # using the raw Hackney client and cannot leverage our + # EncodeUrl Tesla middleware + # Also do it for test environment + defp maybe_encode_url(url) do + case Application.get_env(:tesla, :adapter) do + Tesla.Adapter.Hackney -> Pleroma.HTTP.encode_url(url) + {Tesla.Adapter.Finch, _} -> Pleroma.HTTP.encode_url(url) + Tesla.Mock -> Pleroma.HTTP.encode_url(url) + _ -> url + end + end enddiff --git a/lib/pleroma/safe_zip.ex b/lib/pleroma/safe_zip.ex@@ -0,0 +1,212 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors <https://akkoma.dev/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SafeZip do + @moduledoc """ + Wraps the subset of Erlang's zip module we’d like to use + but enforces path-traversal safety everywhere and other checks. + + For convenience almost all functions accept both elixir strings and charlists, + but output elixir strings themselves. However, this means the input parameter type + can no longer be used to distinguish archive file paths from archive binary data in memory, + thus where needed both a _data and _file variant are provided. + """ + + @type text() :: String.t() | [char()] + + defp safe_path?(path) do + # Path accepts elixir’s chardata() + case Path.safe_relative(path) do + {:ok, _} -> true + _ -> false + end + end + + defp safe_type?(file_type) do + if file_type in [:regular, :directory] do + true + else + false + end + end + + defp maybe_add_file(_type, _path_charlist, nil), do: nil + + defp maybe_add_file(:regular, path_charlist, file_list), + do: [to_string(path_charlist) | file_list] + + defp maybe_add_file(_type, _path_charlist, file_list), do: file_list + + @spec check_safe_archive_and_maybe_list_files(binary() | [char()], [term()], boolean()) :: + {:ok, [String.t()]} | {:error, reason :: term()} + defp check_safe_archive_and_maybe_list_files(archive, opts, list) do + acc = if list, do: [], else: nil + + with {:ok, table} <- :zip.table(archive, opts) do + Enum.reduce_while(table, {:ok, acc}, fn + # ZIP comment + {:zip_comment, _}, acc -> + {:cont, acc} + + # File entry + {:zip_file, path, info, _comment, _offset, _comp_size}, {:ok, fl} -> + with {_, type} <- {:get_type, elem(info, 2)}, + {_, true} <- {:type, safe_type?(type)}, + {_, true} <- {:safe_path, safe_path?(path)} do + {:cont, {:ok, maybe_add_file(type, path, fl)}} + else + {:type, _} -> + {:halt, {:error, "Potentially unsafe file type in ZIP at: #{path}"}} + + {:safe_path, _} -> + {:halt, {:error, "Unsafe path in ZIP: #{path}"}} + end + + # new OTP version? + _, _acc -> + {:halt, {:error, "Unknown ZIP record type"}} + end) + end + end + + @spec check_safe_archive_and_list_files(binary() | [char()], [term()]) :: + {:ok, [String.t()]} | {:error, reason :: term()} + defp check_safe_archive_and_list_files(archive, opts \\ []) do + check_safe_archive_and_maybe_list_files(archive, opts, true) + end + + @spec check_safe_archive(binary() | [char()], [term()]) :: :ok | {:error, reason :: term()} + defp check_safe_archive(archive, opts \\ []) do + case check_safe_archive_and_maybe_list_files(archive, opts, false) do + {:ok, _} -> :ok + error -> error + end + end + + @spec check_safe_file_list([text()], text()) :: :ok | {:error, term()} + defp check_safe_file_list([], _), do: :ok + + defp check_safe_file_list([path | tail], cwd) do + with {_, true} <- {:path, safe_path?(path)}, + {_, {:ok, fstat}} <- {:stat, File.stat(Path.expand(path, cwd))}, + {_, true} <- {:type, safe_type?(fstat.type)} do + check_safe_file_list(tail, cwd) + else + {:path, _} -> + {:error, "Unsafe path escaping cwd: #{path}"} + + {:stat, e} -> + {:error, "Unable to check file type of #{path}: #{inspect(e)}"} + + {:type, _} -> + {:error, "Unsafe type at #{path}"} + end + end + + defp check_safe_file_list(_, _), do: {:error, "Malformed file_list"} + + @doc """ + Checks whether the archive data contais file entries for all paths from fset + + Note this really only accepts entries corresponding to regular _files_, + if a path is contained as for example an directory, this does not count as a match. + """ + @spec contains_all_data?(binary(), MapSet.t()) :: true | false + def contains_all_data?(archive_data, fset) do + with {:ok, table} <- :zip.table(archive_data) do + remaining = + Enum.reduce(table, fset, fn + {:zip_file, path, info, _comment, _offset, _comp_size}, fset -> + if elem(info, 2) == :regular do + MapSet.delete(fset, path) + else + fset + end + + _, _ -> + fset + end) + |> MapSet.size() + + if remaining == 0, do: true, else: false + else + _ -> false + end + end + + @doc """ + List all file entries in ZIP, or error if invalid or unsafe. + + Note this really only lists regular files, no directories, ZIP comments or other types! + """ + @spec list_dir_file(text()) :: {:ok, [String.t()]} | {:error, reason :: term()} + def list_dir_file(archive) do + path = to_charlist(archive) + check_safe_archive_and_list_files(path) + end + + defp stringify_zip({:ok, {fname, data}}), do: {:ok, {to_string(fname), data}} + defp stringify_zip({:ok, fname}), do: {:ok, to_string(fname)} + defp stringify_zip(ret), do: ret + + @spec zip(text(), text(), [text()], boolean()) :: + {:ok, file_name :: String.t()} + | {:ok, {file_name :: String.t(), file_data :: binary()}} + | {:error, reason :: term()} + def zip(name, file_list, cwd, memory \\ false) do + opts = [{:cwd, to_charlist(cwd)}] + opts = if memory, do: [:memory | opts], else: opts + + with :ok <- check_safe_file_list(file_list, cwd) do + file_list = for f <- file_list, do: to_charlist(f) + name = to_charlist(name) + stringify_zip(:zip.zip(name, file_list, opts)) + end + end + + @spec unzip_file(text(), text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + def unzip_file(archive, target_dir, file_list \\ nil) do + do_unzip(to_charlist(archive), to_charlist(target_dir), file_list) + end + + @spec unzip_data(binary(), text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + def unzip_data(archive, target_dir, file_list \\ nil) do + do_unzip(archive, to_charlist(target_dir), file_list) + end + + defp stringify_unzip({:ok, [{_fname, _data} | _] = filebinlist}), + do: {:ok, Enum.map(filebinlist, fn {fname, data} -> {to_string(fname), data} end)} + + defp stringify_unzip({:ok, [_fname | _] = filelist}), + do: {:ok, Enum.map(filelist, fn fname -> to_string(fname) end)} + + defp stringify_unzip({:error, {fname, term}}), do: {:error, {to_string(fname), term}} + defp stringify_unzip(ret), do: ret + + @spec do_unzip(binary() | [char()], text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + defp do_unzip(archive, target_dir, file_list) do + opts = + if file_list != nil do + [ + file_list: for(f <- file_list, do: to_charlist(f)), + cwd: target_dir + ] + else + [cwd: target_dir] + end + + with :ok <- check_safe_archive(archive) do + stringify_unzip(:zip.unzip(archive, opts)) + end + end +enddiff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex@@ -102,7 +102,8 @@ defmodule Pleroma.Search.DatabaseSearch do ^tsc, o.data, ^search_query - ) + ), + order_by: [desc: :inserted_at] ) enddiff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex@@ -122,6 +122,7 @@ defmodule Pleroma.Search.Meilisearch do # Only index public or unlisted Notes if not is_nil(object) and object.data["type"] == "Note" and not is_nil(object.data["content"]) and + not is_nil(object.data["published"]) and (Pleroma.Constants.as_public() in object.data["to"] or Pleroma.Constants.as_public() in object.data["cc"]) and object.data["content"] not in ["", "."] dodiff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex@@ -157,26 +157,55 @@ defmodule Pleroma.Search.QdrantSearch do end defmodule Pleroma.Search.QdrantSearch.OpenAIClient do - use Tesla alias Pleroma.Config.Getting, as: Config - plug(Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :openai_url])) - plug(Tesla.Middleware.JSON) + def post(path, body) do + Tesla.post(client(), path, body) + end + + defp client do + Tesla.client(middleware()) + end - plug(Tesla.Middleware.Headers, [ - {"Authorization", - "Bearer #{Pleroma.Config.get([Pleroma.Search.QdrantSearch, :openai_api_key])}"} - ]) + defp middleware do + [ + {Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :openai_url])}, + Tesla.Middleware.JSON, + {Tesla.Middleware.Headers, + [ + {"Authorization", "Bearer #{Config.get([Pleroma.Search.QdrantSearch, :openai_api_key])}"} + ]} + ] + end end defmodule Pleroma.Search.QdrantSearch.QdrantClient do - use Tesla alias Pleroma.Config.Getting, as: Config - plug(Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :qdrant_url])) - plug(Tesla.Middleware.JSON) + def delete(path) do + Tesla.delete(client(), path) + end + + def post(path, body) do + Tesla.post(client(), path, body) + end + + def put(path, body) do + Tesla.put(client(), path, body) + end - plug(Tesla.Middleware.Headers, [ - {"api-key", Pleroma.Config.get([Pleroma.Search.QdrantSearch, :qdrant_api_key])} - ]) + defp client do + Tesla.client(middleware()) + end + + defp middleware do + [ + {Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :qdrant_url])}, + Tesla.Middleware.JSON, + {Tesla.Middleware.Headers, + [ + {"api-key", Pleroma.Config.get([Pleroma.Search.QdrantSearch, :qdrant_api_key])} + ]} + ] + end enddiff --git a/lib/pleroma/tesla/middleware/encode_url.ex b/lib/pleroma/tesla/middleware/encode_url.ex@@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2025 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Tesla.Middleware.EncodeUrl do + @moduledoc """ + Middleware to encode URLs properly + + We must decode and then re-encode to ensure correct encoding. + If we only encode it will re-encode each % as %25 causing a space + already encoded as %20 to be %2520. + + Similar problem for query parameters which need spaces to be the + character + """ + + @behaviour Tesla.Middleware + + @impl Tesla.Middleware + def call(%Tesla.Env{url: url} = env, next, _) do + url = Pleroma.HTTP.encode_url(url) + + env = %{env | url: url} + + case Tesla.run(env, next) do + {:ok, env} -> {:ok, env} + err -> err + end + end +enddiff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex@@ -90,9 +90,13 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do {:ok, rgb} = if Image.has_alpha?(resized_image) do # remove alpha channel - resized_image - |> Operation.extract_band!(0, n: 3) - |> Image.write_to_binary() + case Operation.extract_band(resized_image, 0, n: 3) do + {:ok, data} -> + Image.write_to_binary(data) + + _ -> + Image.write_to_binary(resized_image) + end else Image.write_to_binary(resized_image) enddiff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex@@ -10,7 +10,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do """ @behaviour Pleroma.Upload.Filter - alias Pleroma.Config + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) alias Pleroma.Upload def filter(%Upload{name: name} = upload) do @@ -23,7 +23,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do @spec predefined_name(String.t()) :: String.t() | nil defp predefined_name(extension) do - with name when not is_nil(name) <- Config.get([__MODULE__, :text]), + with name when not is_nil(name) <- @config_impl.get([__MODULE__, :text]), do: String.replace(name, "{extension}", extension) enddiff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex@@ -17,8 +17,16 @@ defmodule Pleroma.Upload.Filter.Dedupe do |> Base.encode16(case: :lower) filename = shasum <> "." <> extension - {:ok, :filtered, %Upload{upload | id: shasum, path: filename}} + + {:ok, :filtered, %Upload{upload | id: shasum, path: shard_path(filename)}} end def filter(_), do: {:ok, :noop} + + @spec shard_path(String.t()) :: String.t() + def shard_path( + <<a::binary-size(2), b::binary-size(2), c::binary-size(2), _::binary>> = filename + ) do + Path.join([a, b, c, filename]) + end enddiff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex@@ -8,9 +8,16 @@ defmodule Pleroma.Upload.Filter.Mogrify do @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + @mogrify_impl Application.compile_env( + :pleroma, + [__MODULE__, :mogrify_impl], + Pleroma.MogrifyWrapper + ) + def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do try do - do_filter(file, Pleroma.Config.get!([__MODULE__, :args])) + do_filter(file, @config_impl.get!([__MODULE__, :args])) {:ok, :filtered} rescue e in ErlangError -> @@ -22,9 +29,9 @@ defmodule Pleroma.Upload.Filter.Mogrify do def do_filter(file, filters) do file - |> Mogrify.open() + |> @mogrify_impl.open() |> mogrify_filter(filters) - |> Mogrify.save(in_place: true) + |> @mogrify_impl.save(in_place: true) end defp mogrify_filter(mogrify, nil), do: mogrify @@ -38,10 +45,10 @@ defmodule Pleroma.Upload.Filter.Mogrify do defp mogrify_filter(mogrify, []), do: mogrify defp mogrify_filter(mogrify, {action, options}) do - Mogrify.custom(mogrify, action, options) + @mogrify_impl.custom(mogrify, action, options) end defp mogrify_filter(mogrify, action) when is_binary(action) do - Mogrify.custom(mogrify, action) + @mogrify_impl.custom(mogrify, action) end enddiff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex@@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do [file | folders] -> path = Path.join([upload_path()] ++ Enum.reverse(folders)) - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) {path, file} enddiff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex@@ -19,6 +19,7 @@ defmodule Pleroma.User do alias Pleroma.Emoji alias Pleroma.FollowingRelationship alias Pleroma.Formatter + alias Pleroma.Hashtag alias Pleroma.HTML alias Pleroma.Keys alias Pleroma.MFA @@ -27,6 +28,7 @@ defmodule Pleroma.User do alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User + alias Pleroma.User.HashtagFollow alias Pleroma.UserRelationship alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -148,7 +150,7 @@ defmodule Pleroma.User do field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) field(:actor_type, :string, default: "Person") - field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: []) + field(:also_known_as, {:array, ObjectValidators.BareUri}, default: []) field(:inbox, :string) field(:shared_inbox, :string) field(:accepts_chat_messages, :boolean, default: nil) @@ -174,6 +176,12 @@ defmodule Pleroma.User do has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id) has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id) + many_to_many(:followed_hashtags, Hashtag, + on_replace: :delete, + on_delete: :delete_all, + join_through: HashtagFollow + ) + for {relationship_type, [ {outgoing_relation, outgoing_relation_target}, @@ -300,7 +308,7 @@ defmodule Pleroma.User do def binary_id(%User{} = user), do: binary_id(user.id) - @doc "Returns status account" + @doc "Returns account status" @spec account_status(User.t()) :: account_status() def account_status(%User{is_active: false}), do: :deactivated def account_status(%User{password_reset_pending: true}), do: :password_reset_pending @@ -419,6 +427,11 @@ defmodule Pleroma.User do end end + def image_description(image, default \\ "") + + def image_description(%{"name" => name}, _default), do: name + def image_description(_, default), do: default + # Should probably be renamed or removed @spec ap_id(User.t()) :: String.t() def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}" @@ -882,7 +895,7 @@ defmodule Pleroma.User do end) end - def validate_email_not_in_blacklisted_domain(changeset, field) do + defp validate_email_not_in_blacklisted_domain(changeset, field) do validate_change(changeset, field, fn _, value -> valid? = Config.get([User, :email_blacklist]) @@ -899,9 +912,9 @@ defmodule Pleroma.User do end) end - def maybe_validate_required_email(changeset, true), do: changeset + defp maybe_validate_required_email(changeset, true), do: changeset - def maybe_validate_required_email(changeset, _) do + defp maybe_validate_required_email(changeset, _) do if Config.get([:instance, :account_activation_required]) do validate_required(changeset, [:email]) else @@ -1096,15 +1109,15 @@ defmodule Pleroma.User do defp maybe_send_registration_email(_), do: {:ok, :noop} - def needs_update?(%User{local: true}), do: false + defp needs_update?(%User{local: true}), do: false - def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true + defp needs_update?(%User{local: false, last_refreshed_at: nil}), do: true - def needs_update?(%User{local: false} = user) do + defp needs_update?(%User{local: false} = user) do NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400 end - def needs_update?(_), do: true + defp needs_update?(_), do: true @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()} @@ -1695,7 +1708,9 @@ defmodule Pleroma.User do end end - def block(%User{} = blocker, %User{} = blocked) do + def block(blocker, blocked, params \\ %{}) + + def block(%User{} = blocker, %User{} = blocked, params) do # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) blocker = if following?(blocker, blocked) do @@ -1725,12 +1740,33 @@ defmodule Pleroma.User do {:ok, blocker} = update_follower_count(blocker) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) - add_to_block(blocker, blocked) + + duration = Map.get(params, :duration, 0) + + expires_at = + if duration > 0 do + DateTime.utc_now() + |> DateTime.add(duration) + else + nil + end + + user_block = add_to_block(blocker, blocked, expires_at) + + if duration > 0 do + Pleroma.Workers.MuteExpireWorker.new( + %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id}, + scheduled_at: expires_at + ) + |> Oban.insert() + end + + user_block end # helper to handle the block given only an actor's AP id - def block(%User{} = blocker, %{ap_id: ap_id}) do - block(blocker, get_cached_by_ap_id(ap_id)) + def block(%User{} = blocker, %{ap_id: ap_id}, params) do + block(blocker, get_cached_by_ap_id(ap_id), params) end def unblock(%User{} = blocker, %User{} = blocked) do @@ -1971,7 +2007,7 @@ defmodule Pleroma.User do end @spec purge_user_changeset(User.t()) :: Ecto.Changeset.t() - def purge_user_changeset(user) do + defp purge_user_changeset(user) do # "Right to be forgotten" # https://gdpr.eu/right-to-be-forgotten/ change(user, %{ @@ -2143,7 +2179,7 @@ defmodule Pleroma.User do Repo.all(query) end - def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do + defp delete_notifications_from_user_activities(%User{ap_id: ap_id}) do Notification |> join(:inner, [n], activity in assoc(n, :activity)) |> where([n, a], fragment("? = ?", a.actor, ^ap_id)) @@ -2602,7 +2638,7 @@ defmodule Pleroma.User do end end - # Internal function; public one is `deactivate/2` + # Internal function; public one is `set_activation/2` defp set_activation_status(user, status) do user |> cast(%{is_active: status}, [:is_active]) @@ -2621,7 +2657,7 @@ defmodule Pleroma.User do |> update_and_set_cache() end - def validate_fields(changeset, remote? \\ false) do + defp validate_fields(changeset, remote?) do limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit = Config.get([:instance, limit_name], 0) @@ -2766,10 +2802,10 @@ defmodule Pleroma.User do set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) end - @spec add_to_block(User.t(), User.t()) :: + @spec add_to_block(User.t(), User.t(), integer() | nil) :: {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} - defp add_to_block(%User{} = user, %User{} = blocked) do - with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do + defp add_to_block(%User{} = user, %User{} = blocked, expires_at) do + with {:ok, relationship} <- UserRelationship.create_block(user, blocked, expires_at) do @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}") {:ok, relationship} end @@ -2856,4 +2892,54 @@ defmodule Pleroma.User do birthday_month: month }) end + + defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user) + when is_list(follows), + do: user + + defp maybe_load_followed_hashtags(%User{} = user) do + followed_hashtags = HashtagFollow.get_by_user(user) + %{user | followed_hashtags: followed_hashtags} + end + + def followed_hashtags(%User{followed_hashtags: follows}) + when is_list(follows), + do: follows + + def followed_hashtags(%User{} = user) do + {:ok, user} = + user + |> maybe_load_followed_hashtags() + |> set_cache() + + user.followed_hashtags + end + + def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do + Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}") + user = maybe_load_followed_hashtags(user) + + with {:ok, _} <- HashtagFollow.new(user, hashtag), + follows <- HashtagFollow.get_by_user(user), + %User{} = user <- user |> Map.put(:followed_hashtags, follows) do + user + |> set_cache() + end + end + + def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do + Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}") + user = maybe_load_followed_hashtags(user) + + with {:ok, _} <- HashtagFollow.delete(user, hashtag), + follows <- HashtagFollow.get_by_user(user), + %User{} = user <- user |> Map.put(:followed_hashtags, follows) do + user + |> set_cache() + end + end + + def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do + not is_nil(HashtagFollow.get(user, hashtag)) + end enddiff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex@@ -16,6 +16,7 @@ defmodule Pleroma.User.Backup do alias Pleroma.Bookmark alias Pleroma.Config alias Pleroma.Repo + alias Pleroma.SafeZip alias Pleroma.Uploaders.Uploader alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -179,12 +180,12 @@ defmodule Pleroma.User.Backup do end @files [ - ~c"actor.json", - ~c"outbox.json", - ~c"likes.json", - ~c"bookmarks.json", - ~c"followers.json", - ~c"following.json" + "actor.json", + "outbox.json", + "likes.json", + "bookmarks.json", + "followers.json", + "following.json" ] @spec run(t()) :: {:ok, t()} | {:error, :failed} @@ -192,7 +193,7 @@ defmodule Pleroma.User.Backup do backup = Repo.preload(backup, :user) tempfile = Path.join([backup.tempdir, backup.file_name]) - with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)}, + with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)}, {_, :ok} <- {:actor, actor(backup.tempdir, backup.user)}, {_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)}, {_, :ok} <- {:likes, likes(backup.tempdir, backup.user)}, @@ -200,7 +201,7 @@ defmodule Pleroma.User.Backup do {_, :ok} <- {:followers, followers(backup.tempdir, backup.user)}, {_, :ok} <- {:following, following(backup.tempdir, backup.user)}, {_, {:ok, _zip_path}} <- - {:zip, :zip.create(to_charlist(tempfile), @files, cwd: to_charlist(backup.tempdir))}, + {:zip, SafeZip.zip(tempfile, @files, backup.tempdir)}, {_, {:ok, %File.Stat{size: zip_size}}} <- {:filestat, File.stat(tempfile)}, {:ok, updated_backup} <- update_record(backup, %{file_size: zip_size}) do {:ok, updated_backup} @@ -246,7 +247,13 @@ defmodule Pleroma.User.Backup do defp actor(dir, user) do with {:ok, json} <- UserView.render("user.json", %{user: user}) - |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) + |> Map.merge(%{ + "bookmarks" => "bookmarks.json", + "likes" => "likes.json", + "outbox" => "outbox.json", + "followers" => "followers.json", + "following" => "following.json" + }) |> Jason.encode() do File.write(Path.join(dir, "actor.json"), json) enddiff --git a/lib/pleroma/user/hashtag_follow.ex b/lib/pleroma/user/hashtag_follow.ex@@ -0,0 +1,55 @@ +defmodule Pleroma.User.HashtagFollow do + use Ecto.Schema + import Ecto.Query + import Ecto.Changeset + + alias Pleroma.Hashtag + alias Pleroma.Repo + alias Pleroma.User + + schema "user_follows_hashtag" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:hashtag, Hashtag) + end + + def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do + user_hashtag_follow + |> cast(attrs, [:user_id, :hashtag_id]) + |> unique_constraint(:hashtag_id, + name: :user_hashtag_follows_user_id_hashtag_id_index, + message: "already following" + ) + |> validate_required([:user_id, :hashtag_id]) + end + + def new(%User{} = user, %Hashtag{} = hashtag) do + %__MODULE__{} + |> changeset(%{user_id: user.id, hashtag_id: hashtag.id}) + |> Repo.insert(on_conflict: :nothing) + end + + def delete(%User{} = user, %Hashtag{} = hashtag) do + with %__MODULE__{} = user_hashtag_follow <- get(user, hashtag) do + Repo.delete(user_hashtag_follow) + else + _ -> {:ok, nil} + end + end + + def get(%User{} = user, %Hashtag{} = hashtag) do + from(hf in __MODULE__) + |> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id) + |> Repo.one() + end + + def get_by_user(%User{} = user) do + user + |> followed_hashtags_query() + |> Repo.all() + end + + def followed_hashtags_query(%User{} = user) do + Ecto.assoc(user, :followed_hashtags) + |> Ecto.Query.order_by([h], desc: h.id) + end +enddiff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex@@ -55,9 +55,13 @@ defmodule Pleroma.UserRelationship do def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__() + def datetime_impl do + Application.get_env(:pleroma, :datetime_impl, Pleroma.DateTime.Impl) + end + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do user_relationship - |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at]) + |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at, :inserted_at]) |> validate_required([:relationship_type, :source_id, :target_id]) |> unique_constraint(:relationship_type, name: :user_relationships_source_id_relationship_type_target_id_index @@ -65,6 +69,7 @@ defmodule Pleroma.UserRelationship do |> validate_not_self_relationship() end + @spec exists?(any(), Pleroma.User.t(), Pleroma.User.t()) :: boolean() def exists?(relationship_type, %User{} = source, %User{} = target) do UserRelationship |> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id) @@ -90,7 +95,8 @@ defmodule Pleroma.UserRelationship do relationship_type: relationship_type, source_id: source.id, target_id: target.id, - expires_at: expires_at + expires_at: expires_at, + inserted_at: datetime_impl().utc_now() }) |> Repo.insert( on_conflict: {:replace_all_except, [:id, :inserted_at]}, @@ -187,7 +193,8 @@ defmodule Pleroma.UserRelationship do {[:mute], []} nil -> - {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} + {[:block, :mute, :notification_mute, :reblog_mute, :endorsement], + [:block, :inverse_subscription]} unknown -> raise "Unsupported :subset option value: #{inspect(unknown)}"diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex@@ -924,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end + # Essentially, either look for activities addressed to `recipients`, _OR_ ones + # that reference a hashtag that the user follows + # Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't + # follow any + defp restrict_recipients_or_hashtags(query, recipients, user, nil) do + restrict_recipients(query, recipients, user) + end + + defp restrict_recipients_or_hashtags(query, recipients, user, []) do + restrict_recipients(query, recipients, user) + end + + defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do + from([activity, object] in query) + |> join(:left, [activity, object], hto in "hashtags_objects", + on: hto.object_id == object.id, + as: :hto + ) + |> where( + [activity, object, hto: hto], + (hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or + fragment("? && ?", ^recipients, activity.recipients) + ) + end + defp restrict_local(query, %{local_only: true}) do from(activity in query, where: activity.local == true) end @@ -1414,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) - |> restrict_recipients(recipients, opts[:user]) + |> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags]) |> restrict_replies(opts) |> restrict_since(opts) |> restrict_local(opts) @@ -1542,16 +1567,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp get_actor_url(_url), do: nil - defp normalize_image(%{"url" => url}) do + defp normalize_image(%{"url" => url} = data) do %{ "type" => "Image", "url" => [%{"href" => url}] } + |> maybe_put_description(data) end defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(_), do: nil + defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do + Map.put(map, "name", description) + end + + defp maybe_put_description(map, _), do: map + defp object_to_user_data(data, additional) do fields = datadiff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex@@ -53,7 +53,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ) plug(:log_inbox_metadata when action in [:inbox]) - plug(:set_requester_reachable when action in [:inbox]) plug(:relay_active? when action in [:relay]) defp relay_active?(conn, _) do @@ -274,13 +273,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do - with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname), - {:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]), + with {:recipient_exists, %User{} = recipient} <- + {:recipient_exists, User.get_cached_by_nickname(nickname)}, + {:sender_exists, {:ok, %User{} = actor}} <- + {:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])}, + {:recipient_active, true} <- {:recipient_active, recipient.is_active}, + {:sender_active, true} <- {:sender_active, actor.is_active}, true <- Utils.recipient_in_message(recipient, actor, params), params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do Federator.incoming_ap_doc(params) json(conn, "ok") else + {:recipient_exists, _} -> + conn + |> put_status(:not_found) + |> json("User does not exist") + + {:sender_exists, _} -> + conn + |> put_status(:not_found) + |> json("Sender does not exist") + + {:recipient_active, _} -> + conn + |> put_status(:not_found) + |> json("User deactivated") + + {:sender_active, _} -> + conn + |> put_status(:not_found) + |> json("Sender deactivated") + _ -> conn |> put_status(:bad_request) @@ -482,7 +505,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_status(:forbidden) |> json(message) - {:error, message} -> + {:error, message} when is_binary(message) -> conn |> put_status(:bad_request) |> json(message) @@ -520,15 +543,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(dgettext("errors", "error")) end - defp set_requester_reachable(%Plug.Conn{} = conn, _) do - with actor <- conn.params["actor"], - true <- is_binary(actor) do - Pleroma.Instances.set_reachable(actor) - end - - conn - end - defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do Logger.metadata(actor: actor, type: type) conndiff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex@@ -327,8 +327,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do }, []} end - @spec block(User.t(), User.t()) :: {:ok, map(), keyword()} - def block(blocker, blocked) do + @spec block(User.t(), User.t(), map()) :: {:ok, map(), keyword()} + def block(blocker, blocked, params \\ %{}) do {:ok, %{ "id" => Utils.generate_activity_id(), @@ -336,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do "actor" => blocker.ap_id, "object" => blocked.ap_id, "to" => [blocked.ap_id] - }, []} + }, Keyword.new(params)} end @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex@@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do def filter(%{} = object), do: get_policies() |> filter(object) + def id_filter(policies, id) when is_binary(id) do + policies + |> Enum.filter(&function_exported?(&1, :id_filter, 1)) + |> Enum.all?(& &1.id_filter(id)) + end + + def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id) + @impl true def pipeline_filter(%{} = message, meta) do object = meta[:object_data]diff --git a/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex b/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex@@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do - @moduledoc """ - Dynamic activity filtering based on an RBL database - - This MRF makes queries to a custom DNS server which will - respond with values indicating the classification of the domain - the activity originated from. This method has been widely used - in the email anti-spam industry for very fast reputation checks. - - e.g., if the DNS response is 127.0.0.1 or empty, the domain is OK - Other values such as 127.0.0.2 may be used for specific classifications. - - Information for why the host is blocked can be stored in a corresponding TXT record. - - This method is fail-open so if the queries fail the activites are accepted. - - An example of software meant for this purpsoe is rbldnsd which can be found - at http://www.corpit.ru/mjt/rbldnsd.html or mirrored at - https://git.pleroma.social/feld/rbldnsd - - It is highly recommended that you run your own copy of rbldnsd and use an - external mechanism to sync/share the contents of the zone file. This is - important to keep the latency on the queries as low as possible and prevent - your DNS server from being attacked so it fails and content is permitted. - """ - - @behaviour Pleroma.Web.ActivityPub.MRF.Policy - - alias Pleroma.Config - - require Logger - - @query_retries 1 - @query_timeout 500 - - @impl true - def filter(%{"actor" => actor} = activity) do - actor_info = URI.parse(actor) - - with {:ok, activity} <- check_rbl(actor_info, activity) do - {:ok, activity} - else - _ -> {:reject, "[DNSRBLPolicy]"} - end - end - - @impl true - def filter(activity), do: {:ok, activity} - - @impl true - def describe do - mrf_dnsrbl = - Config.get(:mrf_dnsrbl) - |> Enum.into(%{}) - - {:ok, %{mrf_dnsrbl: mrf_dnsrbl}} - end - - @impl true - def config_description do - %{ - key: :mrf_dnsrbl, - related_policy: "Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy", - label: "MRF DNSRBL", - description: "DNS RealTime Blackhole Policy", - children: [ - %{ - key: :nameserver, - type: {:string}, - description: "DNSRBL Nameserver to Query (IP or hostame)", - suggestions: ["127.0.0.1"] - }, - %{ - key: :port, - type: {:string}, - description: "Nameserver port", - suggestions: ["53"] - }, - %{ - key: :zone, - type: {:string}, - description: "Root zone for querying", - suggestions: ["bl.pleroma.com"] - } - ] - } - end - - defp check_rbl(%{host: actor_host}, activity) do - with false <- match?(^actor_host, Pleroma.Web.Endpoint.host()), - zone when not is_nil(zone) <- Keyword.get(Config.get([:mrf_dnsrbl]), :zone) do - query = - Enum.join([actor_host, zone], ".") - |> String.to_charlist() - - rbl_response = rblquery(query) - - if Enum.empty?(rbl_response) do - {:ok, activity} - else - Task.start(fn -> - reason = - case rblquery(query, :txt) do - [[result]] -> result - _ -> "undefined" - end - - Logger.warning( - "DNSRBL Rejected activity from #{actor_host} for reason: #{inspect(reason)}" - ) - end) - - :error - end - else - _ -> {:ok, activity} - end - end - - defp get_rblhost_ip(rblhost) do - case rblhost |> String.to_charlist() |> :inet_parse.address() do - {:ok, _} -> rblhost |> String.to_charlist() |> :inet_parse.address() - _ -> {:ok, rblhost |> String.to_charlist() |> :inet_res.lookup(:in, :a) |> Enum.random()} - end - end - - defp rblquery(query, type \\ :a) do - config = Config.get([:mrf_dnsrbl]) - - case get_rblhost_ip(config[:nameserver]) do - {:ok, rblnsip} -> - :inet_res.lookup(query, :in, type, - nameservers: [{rblnsip, config[:port]}], - timeout: @query_timeout, - retry: @query_retries - ) - - _ -> - [] - end - end -enddiff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex@@ -14,5 +14,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do end @impl true + def id_filter(id) do + Logger.debug("REJECTING #{id}") + false + end + + @impl true def describe, do: {:ok, %{}} enddiff --git a/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex b/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex@@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.FODirectReply do - @moduledoc """ - FODirectReply alters the scope of replies to activities which are Followers Only to be Direct. The purpose of this policy is to prevent broken threads for followers of the reply author because their response was to a user that they are not also following. - """ - - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Visibility - - @behaviour Pleroma.Web.ActivityPub.MRF.Policy - - @impl true - def filter( - %{ - "type" => "Create", - "to" => to, - "object" => %{ - "actor" => actor, - "type" => "Note", - "inReplyTo" => in_reply_to - } - } = activity - ) do - with true <- is_binary(in_reply_to), - %User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor), - %Object{} = in_reply_to_object <- Object.get_by_ap_id(in_reply_to), - "private" <- Visibility.get_visibility(in_reply_to_object) do - direct_to = to -- [followers_collection] - - updated_activity = - activity - |> Map.put("cc", []) - |> Map.put("to", direct_to) - |> Map.put("directMessage", true) - |> put_in(["object", "cc"], []) - |> put_in(["object", "to"], direct_to) - - {:ok, updated_activity} - else - _ -> {:ok, activity} - end - end - - @impl true - def filter(activity), do: {:ok, activity} - - @impl true - def describe, do: {:ok, %{}} -enddiff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex@@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do @callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()} + @callback id_filter(String.t()) :: boolean() @callback describe() :: {:ok | :error, map()} @callback config_description() :: %{ optional(:children) => [map()], @@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do description: String.t() } @callback history_awareness() :: :auto | :manual - @optional_callbacks config_description: 0, history_awareness: 0 + @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1 enddiff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex@@ -29,15 +29,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do } = activity ) do with true <- is_binary(in_reply_to), - false <- match?([], cc), + true <- Pleroma.Constants.as_public() in to, %User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor) do updated_to = - to - |> Kernel.++([followers_collection]) + [followers_collection | to] |> Kernel.--([Pleroma.Constants.as_public()]) - updated_cc = [Pleroma.Constants.as_public()] + updated_cc = + [Pleroma.Constants.as_public() | cc] + |> Kernel.--([followers_collection]) updated_activity = activitydiff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex@@ -0,0 +1,117 @@ +defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do + @moduledoc "Drop remote reports if they don't contain enough information." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.Config + + @impl true + def filter(%{"type" => "Flag"} = object) do + with {_, false} <- {:local, local?(object)}, + {:ok, _} <- maybe_reject_all(object), + {:ok, _} <- maybe_reject_anonymous(object), + {:ok, _} <- maybe_reject_third_party(object), + {:ok, _} <- maybe_reject_empty_message(object) do + {:ok, object} + else + {:local, true} -> {:ok, object} + {:reject, message} -> {:reject, message} + end + end + + def filter(object), do: {:ok, object} + + defp maybe_reject_all(object) do + if Config.get([:mrf_remote_report, :reject_all]) do + {:reject, "[RemoteReportPolicy] Remote report"} + else + {:ok, object} + end + end + + defp maybe_reject_anonymous(%{"actor" => actor} = object) do + with true <- Config.get([:mrf_remote_report, :reject_anonymous]), + %URI{path: "/actor"} <- URI.parse(actor) do + {:reject, "[RemoteReportPolicy] Anonymous: #{actor}"} + else + _ -> {:ok, object} + end + end + + defp maybe_reject_third_party(%{"object" => objects} = object) do + {_, to} = + case objects do + [head | tail] when is_binary(head) -> {tail, head} + s when is_binary(s) -> {[], s} + _ -> {[], ""} + end + + with true <- Config.get([:mrf_remote_report, :reject_third_party]), + false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do + {:reject, "[RemoteReportPolicy] Third-party: #{to}"} + else + _ -> {:ok, object} + end + end + + defp maybe_reject_empty_message(%{"content" => content} = object) + when is_binary(content) and content != "" do + {:ok, object} + end + + defp maybe_reject_empty_message(object) do + if Config.get([:mrf_remote_report, :reject_empty_message]) do + {:reject, ["RemoteReportPolicy] No content"]} + else + {:ok, object} + end + end + + defp local?(%{"actor" => actor}) do + String.starts_with?(actor, Pleroma.Web.Endpoint.url()) + end + + @impl true + def describe do + mrf_remote_report = + Config.get(:mrf_remote_report) + |> Enum.into(%{}) + + {:ok, %{mrf_remote_report: mrf_remote_report}} + end + + @impl true + def config_description do + %{ + key: :mrf_remote_report, + related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy", + label: "MRF Remote Report", + description: "Drop remote reports if they don't contain enough information.", + children: [ + %{ + key: :reject_all, + type: :boolean, + description: "Reject all remote reports? (this option takes precedence)", + suggestions: [false] + }, + %{ + key: :reject_anonymous, + type: :boolean, + description: "Reject anonymous remote reports?", + suggestions: [true] + }, + %{ + key: :reject_third_party, + type: :boolean, + description: "Reject reports on users from third-party instances?", + suggestions: [true] + }, + %{ + key: :reject_empty_message, + type: :boolean, + description: "Reject remote reports with no message?", + suggestions: [true] + } + ] + } + end +enddiff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex@@ -192,6 +192,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do end @impl true + def id_filter(id) do + host_info = URI.parse(id) + + with {:ok, _} <- check_accept(host_info, %{}), + {:ok, _} <- check_reject(host_info, %{}) do + true + else + _ -> false + end + end + + @impl true def filter(%{"type" => "Delete", "actor" => actor} = activity) do %{host: actor_host} = URI.parse(actor)diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex@@ -20,6 +20,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do String.match?(shortcode, pattern) end + defp reject_emoji?({shortcode, _url}, installed_emoji) do + valid_shortcode? = String.match?(shortcode, ~r/^[a-zA-Z0-9_-]+$/) + + rejected_shortcode? = + [:mrf_steal_emoji, :rejected_shortcodes] + |> Config.get([]) + |> Enum.any?(fn pattern -> shortcode_matches?(shortcode, pattern) end) + + emoji_installed? = Enum.member?(installed_emoji, shortcode) + + !valid_shortcode? or rejected_shortcode? or emoji_installed? + end + defp steal_emoji({shortcode, url}, emoji_dir_path) do url = Pleroma.Web.MediaProxy.url(url) @@ -74,20 +87,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") ) - File.mkdir_p(emoji_dir_path) + Pleroma.Backports.mkdir_p(emoji_dir_path) new_emojis = foreign_emojis - |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end) - |> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end) - |> Enum.filter(fn {shortcode, _url} -> - reject_emoji? = - [:mrf_steal_emoji, :rejected_shortcodes] - |> Config.get([]) - |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end) - - !reject_emoji? - end) + |> Enum.reject(&reject_emoji?(&1, installed_emoji)) |> Enum.map(&steal_emoji(&1, emoji_dir_path)) |> Enum.filter(& &1)diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex@@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating + import Pleroma.Constants, only: [activity_types: 0, object_types: 0] + alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object @@ -24,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator @@ -38,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @impl true def validate(object, meta) + # This overload works together with the InboxGuardPlug + # and ensures that we are not accepting any activity type + # that cannot pass InboxGuardPlug. + # If we want to support any more activity types, make sure to + # add it in Pleroma.Constants's activity_types or object_types, + # and, if applicable, allowed_activity_types_from_strangers. + def validate(%{"type" => type}, _meta) + when type not in activity_types() and type not in object_types(), + do: {:error, :not_allowed_object_type} + def validate(%{"type" => "Block"} = block_activity, meta) do with {:ok, block_activity} <- block_activity @@ -103,7 +116,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do meta ) when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do - with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object), + with {:ok, object_data} <- + object + |> CommonFixes.maybe_add_language_from_activity(create_activity) + |> cast_and_apply_and_stringify_with_history(), meta = Keyword.put(meta, :object_data, object_data), {:ok, create_activity} <- create_activity @@ -153,11 +169,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do ) when objtype in ~w[Question Answer Audio Video Event Article Note Page] do with {_, false} <- {:local, Access.get(meta, :local, false)}, - {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)}, + {_, {:ok, object_data, _}} <- + {:object_validation, + object + |> CommonFixes.maybe_add_language_from_activity(update_activity) + |> validate(meta)}, meta = Keyword.put(meta, :object_data, object_data), {:ok, update_activity} <- update_activity - |> UpdateValidator.cast_and_validate() + |> UpdateValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do update_activity = stringify_keys(update_activity) {:ok, update_activity, meta} @@ -165,7 +185,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do {:local, _} -> with {:ok, object} <- update_activity - |> UpdateValidator.cast_and_validate() + |> UpdateValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} @@ -180,14 +200,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do end def validate(%{"type" => type} = object, meta) - when type in ~w[Accept Reject Follow Update Like EmojiReact Announce + when type in ~w[Accept Reject Follow Like EmojiReact Announce ChatMessage Answer] do validator = case type do "Accept" -> AcceptRejectValidator "Reject" -> AcceptRejectValidator "Follow" -> FollowValidator - "Update" -> UpdateValidator "Like" -> LikeValidator "EmojiReact" -> EmojiReactValidator "Announce" -> AnnounceValidator @@ -204,6 +223,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do end end + def validate(%{"type" => type} = object, meta) when type == "Update" do + with {:ok, object} <- + object + |> UpdateValidator.cast_and_validate(meta) + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do with {:ok, object} <- objectdiff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex@@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def cast_and_apply(data) do data - |> cast_data + |> cast_data() |> apply_action(:insert) end @@ -85,8 +85,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_replies() |> fix_attachments() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() + |> CommonFixes.maybe_add_language() + |> CommonFixes.maybe_add_content_map() end def changeset(struct, data) dodiff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex@@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> fix_url() |> fix_content()diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex@@ -31,6 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do defmacro object_fields do quote bind_quoted: binding() do field(:content, :string) + field(:contentMap, ObjectValidators.ContentLanguageMap) field(:published, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime) @@ -58,6 +59,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:quotes_count, :integer, default: 0) + field(:language, ObjectValidators.LanguageCode) field(:inReplyTo, ObjectValidators.ObjectID) field(:quoteUrl, ObjectValidators.ObjectID) field(:url, ObjectValidators.BareUri)diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex@@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Language.LanguageDetector alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment @@ -11,6 +12,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils + import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode, + only: [good_locale_code?: 1] + + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + require Pleroma.Constants def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do @@ -114,6 +120,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def fix_quote_url(data), do: data + # On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`, + # not a list of users. + # https://github.com/mastodon/mastodon/pull/32007 + def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"]) + + def fix_likes(data), do: data + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md def object_link_tag?(%{ "type" => "Link", @@ -125,4 +138,75 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do end def object_link_tag?(_), do: false + + def maybe_add_language_from_activity(object, activity) do + language = get_language_from_context(activity) + + if language do + Map.put(object, "language", language) + else + object + end + end + + def maybe_add_language(object) do + language = + [ + &get_language_from_context/1, + &get_language_from_content_map/1, + &get_language_from_content/1 + ] + |> Enum.find_value(fn get_language -> + language = get_language.(object) + + if good_locale_code?(language) do + language + else + nil + end + end) + + if language do + Map.put(object, "language", language) + else + object + end + end + + defp get_language_from_context(%{"@context" => context}) when is_list(context) do + case context + |> Enum.find(fn + %{"@language" => language} -> language != "und" + _ -> nil + end) do + %{"@language" => language} -> language + _ -> nil + end + end + + defp get_language_from_context(_), do: nil + + defp get_language_from_content_map(%{"contentMap" => content_map, "content" => source_content}) do + content_groups = Map.to_list(content_map) + + case Enum.find(content_groups, fn {_, content} -> content == source_content end) do + {language, _} -> language + _ -> nil + end + end + + defp get_language_from_content_map(_), do: nil + + defp get_language_from_content(%{"content" => content} = object) do + LanguageDetector.detect("#{object["summary"] || ""} #{content}") + end + + defp get_language_from_content(_), do: nil + + def maybe_add_content_map(%{"language" => language, "content" => content} = object) + when not_empty_string(language) do + Map.put(object, "contentMap", Map.put(%{}, language, content)) + end + + def maybe_add_content_map(object), do: object enddiff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do def cast_and_apply(data) do data - |> cast_data + |> cast_data() |> apply_action(:insert) end @@ -38,6 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do |> validate_data() end + @spec cast_data(map()) :: map() def cast_data(data) do %__MODULE__{} |> changeset(data) @@ -47,7 +48,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() + |> CommonFixes.maybe_add_language() + |> CommonFixes.maybe_add_content_map() end def changeset(struct, data) dodiff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex@@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> fix_closed() enddiff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex@@ -50,13 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do end def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do - name = - cond do - "#" <> name -> name - name -> name - end - |> String.downcase() - + name = String.downcase(name) data = Map.put(data, "name", name) structdiff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex@@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Object + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -31,23 +33,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do |> cast(data, __schema__(:fields)) end - defp validate_data(cng) do + defp validate_data(cng, meta) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Update"]) |> validate_actor_presence() - |> validate_updating_rights() + |> validate_updating_rights(meta) end - def cast_and_validate(data) do + def cast_and_validate(data, meta \\ []) do data |> cast_data - |> validate_data + |> validate_data(meta) end - # For now we only support updating users, and here the rule is easy: - # object id == actor id - def validate_updating_rights(cng) do + def validate_updating_rights(cng, meta) do + if meta[:local] do + validate_updating_rights_local(cng) + else + validate_updating_rights_remote(cng) + end + end + + # For local Updates, verify the actor can edit the object + def validate_updating_rights_local(cng) do + actor = get_field(cng, :actor) + updated_object = get_field(cng, :object) + + if {:ok, actor} == ObjectValidators.ObjectID.cast(updated_object) do + cng + else + with %User{} = user <- User.get_cached_by_ap_id(actor), + {_, %Object{} = orig_object} <- {:object, Object.normalize(updated_object)}, + :ok <- Object.authorize_access(orig_object, user) do + cng + else + _e -> + cng + |> add_error(:object, "Can't be updated by this actor") + end + end + end + + # For remote Updates, verify the host is the same. + def validate_updating_rights_remote(cng) do with actor = get_field(cng, :actor), object = get_field(cng, :object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object),diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex@@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - cc = Map.get(params, :cc, []) + param_cc = Map.get(params, :cc, []) + + original_cc = Map.get(data, "cc", []) + + public_address = Pleroma.Constants.as_public() + + # Ensure unlisted posts don't lose the public address in the cc + # if the param_cc was set + cc = + if public_address in original_cc and public_address not in param_cc do + [public_address | param_cc] + else + param_cc + end json = data @@ -148,17 +161,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {"digest", p.digest} ] ) do - if not is_nil(p.unreachable_since) do - Instances.set_reachable(p.inbox) - end - result else {_post_result, %{status: code} = response} = e -> - if is_nil(p.unreachable_since) do - Instances.set_unreachable(p.inbox) - end - Logger.metadata(activity: p.activity_id, inbox: p.inbox, status: code) Logger.error("Publisher failed to inbox #{p.inbox} with status #{code}") @@ -179,10 +184,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do connection_pool_snooze() e -> - if is_nil(p.unreachable_since) do - Instances.set_unreachable(p.inbox) - end - Logger.metadata(activity: p.activity_id, inbox: p.inbox) Logger.error("Publisher failed to inbox #{p.inbox} #{inspect(e)}") {:error, e} @@ -294,7 +295,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do [priority_recipients, recipients] = recipients(actor, activity) - inboxes = + [priority_inboxes, other_inboxes] = [priority_recipients, recipients] |> Enum.map(fn recipients -> recipients @@ -307,8 +308,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end) Repo.checkout(fn -> - Enum.each(inboxes, fn inboxes -> - Enum.each(inboxes, fn {inbox, unreachable_since} -> + Enum.each([priority_inboxes, other_inboxes], fn inboxes -> + Enum.each(inboxes, fn inbox -> %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) # Get all the recipients on the same host and add them to cc. Otherwise, a remote @@ -318,8 +319,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do __MODULE__.enqueue_one(%{ inbox: inbox, cc: cc, - activity_id: activity.id, - unreachable_since: unreachable_since + activity_id: activity.id }) end) end) @@ -352,12 +352,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do |> Enum.each(fn {inboxes, priority} -> inboxes |> Instances.filter_reachable() - |> Enum.each(fn {inbox, unreachable_since} -> + |> Enum.each(fn inbox -> __MODULE__.enqueue_one( %{ inbox: inbox, - activity_id: activity.id, - unreachable_since: unreachable_since + activity_id: activity.id }, priority: priority )diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex@@ -145,7 +145,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do ) do with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user), %User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do - User.block(blocker, blocked) + User.block(blocker, blocked, Enum.into(meta, %{})) end {:ok, object, meta}diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex@@ -16,12 +16,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator import Ecto.Query + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] require Pleroma.Constants @@ -41,6 +43,38 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_content_map() |> fix_addressing() |> fix_summary() + |> fix_history(&fix_object/1) + end + + defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object) + defp maybe_fix_object(object), do: object + + defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun) + when is_list(list) do + update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end) + end + + defp fix_history(obj, _), do: obj + + defp fix_recursive(obj, fun) do + # unlike Erlang, Elixir does not support recursive inline functions + # which would allow us to avoid reconstructing this on every recursion + rec_fun = fn + obj when is_map(obj) -> fix_recursive(obj, fun) + # there may be simple AP IDs in history (or object field) + obj -> obj + end + + obj + |> fun.() + |> fix_history(rec_fun) + |> then(fn + %{"object" => object} = doc when is_map(object) -> + update_in(doc["object"], rec_fun) + + apdoc -> + apdoc + end) end def fix_summary(%{"summary" => nil} = object) do @@ -166,7 +200,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_quote_url_and_maybe_fetch(object, options \\ []) do quote_url = - case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do + case CommonFixes.fix_quote_url(object) do %{"quoteUrl" => quote_url} -> quote_url _ -> nil end @@ -336,6 +370,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_tag(object), do: object + # prefer content over contentMap + def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object + # content map usually only has one language so this will do for now. def fix_content_map(%{"contentMap" => content_map} = object) do content_groups = Map.to_list(content_map) @@ -370,11 +407,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end) end - def handle_incoming(data, options \\ []) + def handle_incoming(data, options \\ []) do + data + |> fix_recursive(&strip_internal_fields/1) + |> handle_incoming_normalized(options) + end # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do + defp handle_incoming_normalized( + %{"type" => "Flag", "object" => objects, "actor" => actor} = data, + _options + ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -395,16 +439,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}, _options), do: :error - def handle_incoming(%{"id" => ""}, _options), do: :error + defp handle_incoming_normalized(%{"id" => nil}, _options), do: :error + defp handle_incoming_normalized(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, - do: :error - - def handle_incoming( - %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, - options - ) do + defp handle_incoming_normalized(%{"id" => id}, _options) + when is_binary(id) and byte_size(id) < 8, + do: :error + + defp handle_incoming_normalized( + %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, + options + ) do actor = Containment.get_actor(data) data = @@ -446,25 +491,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "star" => "⭐" } - @doc "Rewrite misskey likes into EmojiReacts" - def handle_incoming( - %{ - "type" => "Like", - "_misskey_reaction" => reaction - } = data, - options - ) do + # Rewrite misskey likes into EmojiReacts + defp handle_incoming_normalized( + %{ + "type" => "Like", + "content" => content + } = data, + options + ) + when is_binary(content) do data |> Map.put("type", "EmojiReact") + |> handle_incoming_normalized(options) + end + + defp handle_incoming_normalized( + %{ + "type" => "Like", + "_misskey_reaction" => reaction + } = data, + options + ) do + data |> Map.put("content", @misskey_reactions[reaction] || reaction) - |> handle_incoming(options) + |> handle_incoming_normalized(options) end - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - options - ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do + defp handle_incoming_normalized( + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, + options + ) + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) object = @@ -487,8 +544,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(%{"type" => type} = data, _options) - when type in ~w{Like EmojiReact Announce Add Remove} do + defp handle_incoming_normalized(%{"type" => type} = data, _options) + when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do @@ -498,11 +555,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{"type" => type} = data, - _options - ) - when type in ~w{Update Block Follow Accept Reject} do + defp handle_incoming_normalized( + %{"type" => type} = data, + _options + ) + when type in ~w{Update Block Follow Accept Reject} do + fixed_obj = maybe_fix_object(data["object"]) + data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do @@ -510,10 +570,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{"type" => "Delete"} = data, - _options - ) do + defp handle_incoming_normalized( + %{"type" => "Delete"} = data, + _options + ) do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -536,15 +596,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "object" => followed}, - "actor" => follower, - "id" => id - } = _data, - _options - ) do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "object" => followed}, + "actor" => follower, + "id" => id + } = _data, + _options + ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do @@ -555,46 +615,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => type} - } = data, - _options - ) - when type in ["Like", "EmojiReact", "Announce", "Block"] do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => %{"type" => type} + } = data, + _options + ) + when type in ["Like", "EmojiReact", "Announce", "Block"] do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} end end # For Undos that don't have the complete object attached, try to find it in our database. - def handle_incoming( - %{ - "type" => "Undo", - "object" => object - } = activity, - options - ) - when is_binary(object) do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => object + } = activity, + options + ) + when is_binary(object) do with %Activity{data: data} <- Activity.get_by_ap_id(object) do activity |> Map.put("object", data) - |> handle_incoming(options) + |> handle_incoming_normalized(options) else _e -> :error end end - def handle_incoming( - %{ - "type" => "Move", - "actor" => origin_actor, - "object" => origin_actor, - "target" => target_actor - }, - _options - ) do + defp handle_incoming_normalized( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), true <- origin_actor in target_user.also_known_as do @@ -604,7 +664,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(_, _), do: :error + # Rewrite dislikes into the thumbs down emoji + defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do + data + |> Map.put("type", "EmojiReact") + |> Map.put("content", "👎") + |> handle_incoming_normalized(options) + end + + defp handle_incoming_normalized( + %{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data, + options + ) do + data + |> put_in(["object", "type"], "EmojiReact") + |> put_in(["object", "content"], "👎") + |> handle_incoming_normalized(options) + end + + defp handle_incoming_normalized(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil def get_obj_helper(id, options \\ []) do @@ -716,6 +794,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> set_reply_to_uri |> set_quote_url |> set_replies + |> CommonFixes.maybe_add_content_map() |> strip_internal_fields |> strip_internal_tags |> set_type @@ -750,12 +829,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do object_id |> Object.normalize(fetch: false) |> Map.get(:data) - |> prepare_object data = data - |> Map.put("object", object) - |> Map.merge(Utils.make_json_ld_header()) + |> Map.put("object", prepare_object(object)) + |> Map.merge(Utils.make_json_ld_header(object)) |> Map.delete("bcc") {:ok, data} @@ -763,14 +841,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) when objtype in Pleroma.Constants.updatable_object_types() do - object = - object - |> prepare_object - data = data - |> Map.put("object", object) - |> Map.merge(Utils.make_json_ld_header()) + |> Map.put("object", prepare_object(object)) + |> Map.merge(Utils.make_json_ld_header(object)) |> Map.delete("bcc") {:ok, data} @@ -840,7 +914,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do data |> strip_internal_fields |> maybe_fix_object_url - |> Map.merge(Utils.make_json_ld_header()) + |> Map.merge(Utils.make_json_ld_header(data)) {:ok, data} enddiff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex@@ -20,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Pleroma.Web.Router.Helpers import Ecto.Query + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] require Logger require Pleroma.Constants @@ -109,18 +110,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do end end - def make_json_ld_header do + def make_json_ld_header(data \\ %{}) do %{ "@context" => [ "https://www.w3.org/ns/activitystreams", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ - "@language" => "und" + "@language" => get_language(data) } ] } end + defp get_language(%{"language" => language}) when not_empty_string(language) do + language + end + + defp get_language(_), do: "und" + def make_date do DateTime.utc_now() |> DateTime.to_iso8601() enddiff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do alias Pleroma.Web.ActivityPub.Transmogrifier def render("object.json", %{object: %Object{} = object}) do - base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() + base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data) additional = Transmogrifier.prepare_object(object.data) Map.merge(base, additional) @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity}) when activity_type in ["Create", "Listen"] do - base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() + base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data) object = Object.normalize(activity, fetch: false) additional = @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do end def render("object.json", %{object: %Activity{} = activity}) do - base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() + base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data) object_id = Object.normalize(activity, id_only: true) additional =diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex@@ -127,10 +127,25 @@ defmodule Pleroma.Web.ActivityPub.UserView do "capabilities" => capabilities, "alsoKnownAs" => user.also_known_as, "vcard:bday" => birthday, - "webfinger" => "acct:#{User.full_nickname(user)}" + "webfinger" => "acct:#{User.full_nickname(user)}", + "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) } - |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) - |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) + |> Map.merge( + maybe_make_image( + &User.avatar_url/2, + User.image_description(user.avatar, nil), + "icon", + user + ) + ) + |> Map.merge( + maybe_make_image( + &User.banner_url/2, + User.image_description(user.banner, nil), + "image", + user + ) + ) |> Map.merge(Utils.make_json_ld_header()) end @@ -305,16 +320,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do end end - defp maybe_make_image(func, key, user) do + defp maybe_make_image(func, description, key, user) do if image = func.(user, no_default: true) do %{ - key => %{ - "type" => "Image", - "url" => image - } + key => + %{ + "type" => "Image", + "url" => image + } + |> maybe_put_description(description) } else %{} end end + + defp maybe_put_description(map, description) when is_binary(description) do + Map.put(map, "name", description) + end + + defp maybe_put_description(map, _description), do: map enddiff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex@@ -335,13 +335,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do if params["password"] do User.force_password_reset_async(user) - end - ModerationLog.insert_log(%{ - actor: admin, - subject: [user], - action: "force_password_reset" - }) + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "force_password_reset" + }) + end json(conn, %{status: "success"}) elsediff --git a/lib/pleroma/web/admin_api/controllers/instance_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_controller.ex@@ -49,7 +49,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceController do end def delete(conn, %{"instance" => instance}) do - with {:ok, _job} <- Instance.delete_users_and_activities(instance) do + with {:ok, _job} <- Instance.delete(instance) do json(conn, instance) end enddiff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex@@ -97,7 +97,7 @@ defmodule Pleroma.Web.ApiSpec do "Frontend management", "Instance configuration", "Instance documents", - "Instance rule managment", + "Instance rule management", "Invites", "MediaProxy cache", "OAuth application management", @@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do "Search", "Status actions", "Media attachments", - "Bookmark folders" + "Bookmark folders", + "Tags" ] }, %{ @@ -157,6 +158,6 @@ defmodule Pleroma.Web.ApiSpec do } } # discover request/response schemas from path specs - |> OpenApiSpex.resolve_schema_modules() + |> then(&OpenApiSpex.resolve_schema_modules/1) end enddiff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex@@ -284,18 +284,6 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do :query, %Schema{allOf: [BooleanLike], default: true}, "Mute notifications in addition to statuses? Defaults to `true`." - ), - Operation.parameter( - :duration, - :query, - %Schema{type: :integer}, - "Expire the mute in `duration` seconds. Default 0 for infinity" - ), - Operation.parameter( - :expires_in, - :query, - %Schema{type: :integer, default: 0}, - "Deprecated, use `duration` instead" ) ], responses: %{ @@ -323,16 +311,37 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do tags: ["Account actions"], summary: "Block", operationId: "AccountController.block", + requestBody: request_body("Parameters", block_request()), security: [%{"oAuth" => ["follow", "write:blocks"]}], description: "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)", - parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} + ], responses: %{ 200 => Operation.response("Relationship", "application/json", AccountRelationship) } } end + defp block_request do + %Schema{ + title: "AccountBlockRequest", + description: "POST body for blocking an account", + type: :object, + properties: %{ + duration: %Schema{ + type: :integer, + nullable: true, + description: "Expire the mute in `duration` seconds. Default 0 for infinity" + } + }, + example: %{ + "duration" => 86_400 + } + } + end + def unblock_operation do %Operation{ tags: ["Account actions"],diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def index_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Retrieve list of instance rules", operationId: "AdminAPI.RuleController.index", security: [%{"oAuth" => ["admin:read"]}], @@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def create_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Create new rule", operationId: "AdminAPI.RuleController.create", security: [%{"oAuth" => ["admin:write"]}], @@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def update_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Modify existing rule", operationId: "AdminAPI.RuleController.update", security: [%{"oAuth" => ["admin:write"]}], @@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def delete_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Delete rule", operationId: "AdminAPI.RuleController.delete", parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex@@ -52,7 +52,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do summary: "Retrieve list of instance rules", operationId: "InstanceController.rules", responses: %{ - 200 => Operation.response("Array of domains", "application/json", array_of_rules()) + 200 => Operation.response("Array of rules", "application/json", array_of_rules()) + } + } + end + + def translation_languages_operation do + %Operation{ + tags: ["Instance misc"], + summary: "Retrieve supported languages matrix", + operationId: "InstanceController.translation_languages", + responses: %{ + 200 => + Operation.response( + "Translation languages matrix", + "application/json", + %Schema{ + type: :object, + additionalProperties: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Supported target languages for a source language" + } + } + ) } } enddiff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex@@ -121,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do security: [%{"oAuth" => ["write:media"]}], requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ - 202 => Operation.response("Media", "application/json", Attachment), + 200 => Operation.response("Media", "application/json", Attachment), 400 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError), 500 => Operation.response("Media", "application/json", ApiError)diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex@@ -127,6 +127,20 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do } end + def download_zip_operation do + %Operation{ + tags: ["Emoji pack administration"], + summary: "Download a pack from a URL or an uploaded file", + operationId: "PleromaAPI.EmojiPackController.download_zip", + security: [%{"oAuth" => ["admin:write"]}], + requestBody: request_body("Parameters", download_zip_request(), required: true), + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + defp download_request do %Schema{ type: :object, @@ -143,6 +157,25 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do } end + defp download_zip_request do + %Schema{ + type: :object, + required: [:name], + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + description: "URL of the file" + }, + file: %Schema{ + description: "The uploaded ZIP file", + type: :object + }, + name: %Schema{type: :string, format: :uri, description: "Pack Name"} + } + } + end + def create_operation do %Operation{ tags: ["Emoji pack administration"],diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex@@ -59,11 +59,15 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, length: %Schema{type: :integer, description: "The length of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, visibility: %Schema{ allOf: [VisibilityScope], default: "public", description: "Scrobble visibility" + }, + externalLink: %Schema{ + type: :string, + description: "Deprecated, use `external_link` instead" } }, example: %{ @@ -71,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title" + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title" } } end @@ -85,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do title: %Schema{type: :string, description: "The title of the media playing"}, album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, length: %Schema{ type: :integer, description: "The length of the media playing", @@ -100,7 +104,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title", + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at" => "2019-09-28T12:40:45.000Z" } }diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex@@ -427,6 +427,38 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } end + def translate_operation do + %Operation{ + tags: ["Retrieve status information"], + summary: "Translate status", + description: "Translate status with an external API", + operationId: "StatusController.translate", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [id_param()], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + lang: %Schema{ + type: :string, + nullable: true, + description: "Translation target language." + } + } + }, + required: false + ), + responses: %{ + 200 => Operation.response("Translation", "application/json", translation()), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError), + 503 => Operation.response("Error", "application/json", ApiError) + } + } + end + def favourites_operation do %Operation{ tags: ["Timelines"], @@ -819,4 +851,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } } end + + defp translation do + %Schema{ + title: "StatusTranslation", + description: "Represents status translation with related information.", + type: :object, + required: [:content, :detected_source_language, :provider], + properties: %{ + content: %Schema{ + type: :string, + description: "Translated status content" + }, + detected_source_language: %Schema{ + type: :string, + description: "Detected source language" + }, + provider: %Schema{ + type: :string, + description: "Translation provider service name" + } + }, + example: %{ + "content" => "Software für die nächste Generation der sozialen Medien.", + "detected_source_language" => "en", + "provider" => "Deepl" + } + } + end enddiff --git a/lib/pleroma/web/api_spec/operations/tag_operation.ex b/lib/pleroma/web/api_spec/operations/tag_operation.ex@@ -0,0 +1,103 @@ +defmodule Pleroma.Web.ApiSpec.TagOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Tag + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Tags"], + summary: "Hashtag", + description: "View a hashtag", + security: [%{"oAuth" => ["read"]}], + parameters: [id_param()], + operationId: "TagController.show", + responses: %{ + 200 => Operation.response("Hashtag", "application/json", Tag), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def follow_operation do + %Operation{ + tags: ["Tags"], + summary: "Follow a hashtag", + description: "Follow a hashtag", + security: [%{"oAuth" => ["write:follows"]}], + parameters: [id_param()], + operationId: "TagController.follow", + responses: %{ + 200 => Operation.response("Hashtag", "application/json", Tag), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["Tags"], + summary: "Unfollow a hashtag", + description: "Unfollow a hashtag", + security: [%{"oAuth" => ["write:follows"]}], + parameters: [id_param()], + operationId: "TagController.unfollow", + responses: %{ + 200 => Operation.response("Hashtag", "application/json", Tag), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def show_followed_operation do + %Operation{ + tags: ["Tags"], + summary: "Followed hashtags", + description: "View a list of hashtags the currently authenticated user is following", + parameters: pagination_params(), + security: [%{"oAuth" => ["read:follows"]}], + operationId: "TagController.show_followed", + responses: %{ + 200 => + Operation.response("Hashtags", "application/json", %Schema{ + type: :array, + items: Tag + }), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp id_param do + Operation.parameter( + :id, + :path, + %Schema{type: :string}, + "Name of the hashtag" + ) + end + + def pagination_params do + [ + Operation.parameter(:max_id, :query, :integer, "Return items older than this ID"), + Operation.parameter( + :min_id, + :query, + :integer, + "Return the oldest items newer than this ID" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer, default: 20}, + "Maximum number of items to return. Will be ignored if it's more than 40" + ) + ] + end +enddiff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex@@ -34,6 +34,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do id: FlakeID, locked: %Schema{type: :boolean}, mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, + block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, note: %Schema{type: :string, format: :html}, statuses_count: %Schema{type: :integer}, url: %Schema{type: :string, format: :uri},diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex@@ -17,11 +17,22 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do type: :string, format: :uri, description: "A link to the hashtag on the instance" + }, + following: %Schema{ + type: :boolean, + description: "Whether the authenticated user is following the hashtag" + }, + history: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: + "A list of historical uses of the hashtag (not implemented, for compatibility only)" } }, example: %{ name: "cofe", - url: "https://lain.com/tag/cofe" + url: "https://lain.com/tag/cofe", + following: false } }) enddiff --git a/lib/pleroma/web/api_spec/scopes/compiler.ex b/lib/pleroma/web/api_spec/scopes/compiler.ex@@ -26,7 +26,11 @@ defmodule Pleroma.Web.ApiSpec.Scopes.Compiler do end def extract_all_scopes do - extract_all_scopes_from(Pleroma.Web.ApiSpec.spec()) + try do + extract_all_scopes_from(Pleroma.Web.ApiSpec.spec()) + catch + _, _ -> [] + end end def extract_all_scopes_from(specs) dodiff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex@@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do @callback handle_error(Plug.Conn.t(), any()) :: any() @callback auth_template() :: String.t() | nil @callback oauth_consumer_template() :: String.t() | nil + + @callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) :: + {:ok, Pleroma.User.t()} | {:error, term()} + + @optional_callbacks change_password: 4 enddiff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex@@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.LDAPAuthenticator do + alias Pleroma.LDAP alias Pleroma.User import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1] @@ -19,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])}, {:ok, {name, password}} <- fetch_credentials(conn), - %User{} = user <- GenServer.call(Pleroma.LDAP, {:bind_user, name, password}) do + %User{} = user <- LDAP.bind_user(name, password) do {:ok, user} else {:ldap, _} -> @@ -29,4 +30,13 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do error end end + + def change_password(user, password, new_password, new_password) do + case LDAP.change_password(user.nickname, password, new_password) do + :ok -> {:ok, user} + e -> e + end + end + + def change_password(_, _, _, _), do: {:error, :password_confirmation} enddiff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex@@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.AuthenticationPlug import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] @@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def auth_template, do: nil def oauth_consumer_template, do: nil + + @doc "Changes Pleroma.User password in the database" + def change_password(user, password, new_password, new_password) do + case CommonAPI.Utils.confirm_current_password(user, password) do + {:ok, user} -> + with {:ok, _user} <- + User.reset_password(user, %{ + password: new_password, + password_confirmation: new_password + }) do + {:ok, user} + end + + error -> + error + end + end + + def change_password(_, _, _, _), do: {:error, :password_confirmation} enddiff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex@@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do implementation().oauth_consumer_template() || Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html") end + + @impl true + def change_password(user, password, new_password, new_password_confirmation), + do: implementation().change_password(user, password, new_password, new_password_confirmation) enddiff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex@@ -27,9 +27,9 @@ defmodule Pleroma.Web.CommonAPI do require Logger @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() - def block(blocked, blocker) do - with {:ok, block_data, _} <- Builder.block(blocker, blocked), - {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do + def block(blocked, blocker, params \\ %{}) do + with {:ok, block_data, meta} <- Builder.block(blocker, blocked, params), + {:ok, block, _} <- Pipeline.common_pipeline(block_data, meta ++ [local: true]) do {:ok, block} end enddiff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex@@ -5,12 +5,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Activity alias Pleroma.Conversation.Participation + alias Pleroma.Language.LanguageDetector alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils + import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode, + only: [good_locale_code?: 1] + import Pleroma.Web.Gettext import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] @@ -38,6 +42,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do cc: [], context: nil, sensitive: false, + language: nil, object: nil, preview?: false, changes: %{} @@ -64,6 +69,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> content() |> with_valid(&to_and_cc/1) |> with_valid(&context/1) + |> with_valid(&language/1) |> sensitive() |> with_valid(&object/1) |> preview?() @@ -85,7 +91,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp listen_object(draft) do object = draft.params - |> Map.take([:album, :artist, :title, :length, :externalLink]) + |> Map.take([:album, :artist, :title, :length]) + |> Map.put(:externalLink, Map.get(draft.params, :external_link)) |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", draft.to) @@ -249,6 +256,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | sensitive: sensitive} end + defp language(draft) do + language = + with language <- draft.params[:language], + true <- good_locale_code?(language) do + language + else + _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) + end + + %__MODULE__{draft | language: language} + end + defp object(draft) do emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) @@ -288,6 +307,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do "mediaType" => Utils.get_content_type(draft.params[:content_type]) }) |> Map.put("generator", draft.params[:generator]) + |> Map.put("language", draft.language) %__MODULE__{draft | object: object} enddiff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex@@ -14,6 +14,7 @@ defmodule Pleroma.Web.Endpoint do websocket: [ path: "/", compress: false, + connect_info: [:sec_websocket_protocol], error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []}, fullsweep_after: 20 ]diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex@@ -46,7 +46,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do redirector_with_meta(conn, %{user: user}) else nil -> - redirector(conn, params) + redirector_with_meta(conn, Map.delete(params, "maybe_nickname_or_id")) end enddiff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex@@ -122,6 +122,10 @@ defmodule Pleroma.Web.Federator do Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") {:error, e} + {:reject, reason} = e -> + Logger.debug("Rejected by MRF: #{inspect(reason)}") + {:error, e} + e -> # Just drop those for now Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - if Config.get!([:instance, :public]) do + if not Config.restrict_unauthenticated_access?(:timelines, :local) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") @@ -18,10 +18,12 @@ defmodule Pleroma.Web.Feed.TagController do end defp render_feed(conn, %{"tag" => raw_tag} = params) do + local_only = Config.restrict_unauthenticated_access?(:timelines, :federated) + {format, tag} = parse_tag(raw_tag) activities = - %{type: ["Create"], tag: tag} + %{type: ["Create"], tag: tag, local_only: local_only} |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_activities()diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex@@ -15,11 +15,11 @@ defmodule Pleroma.Web.Feed.UserController do action_fallback(:errors) - def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do + def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname} = params) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) else - _ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil) + _ -> Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, params) end enddiff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex@@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do defp put_file(origin_path, destination_path) do with destination <- instance_static_dir(destination_path), - {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))}, + {_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))}, {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do :ok elsediff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex@@ -501,8 +501,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "POST /api/v1/accounts/:id/block" - def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do + def block( + %{ + assigns: %{user: blocker, account: blocked}, + private: %{open_api_spex: %{body_params: params}} + } = conn, + _params + ) do + with {:ok, _activity} <- CommonAPI.block(blocked, blocker, params) do render(conn, "relationship.json", user: blocker, target: blocked) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -607,7 +613,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do users: users, for: user, as: :user, - embed_relationships: embed_relationships?(params) + embed_relationships: embed_relationships?(params), + blocks: true ) enddiff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex@@ -30,4 +30,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do def rules(conn, _params) do render(conn, "rules.json") end + + @doc "GET /api/v1/instance/translation_languages" + def translation_languages(conn, _params) do + render(conn, "translation_languages.json") + end enddiff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex@@ -53,9 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do ) do attachment_data = Map.put(object.data, "id", object.id) - conn - |> put_status(202) - |> render("attachment.json", %{attachment: attachment_data}) + render(conn, "attachment.json", %{attachment: attachment_data}) end enddiff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Workers.PollWorker action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @@ -27,12 +28,16 @@ defmodule Pleroma.Web.MastodonAPI.PollController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + @poll_refresh_interval 120 @doc "GET /api/v1/polls/:id" def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do - with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- + Activity.get_create_by_object_ap_id_with_object(object.data["id"]), true <- Visibility.visible_for_user?(activity, user) do + maybe_refresh_poll(activity) + try_render(conn, "show.json", %{object: object, for: user}) else error when is_nil(error) or error == false -> @@ -70,4 +75,13 @@ defmodule Pleroma.Web.MastodonAPI.PollController do end end) end + + defp maybe_refresh_poll(%Activity{object: %Object{} = object} = activity) do + with false <- activity.local, + {:ok, end_time} <- NaiveDateTime.from_iso8601(object.data["closed"]), + {_, :lt} <- {:closed_compare, NaiveDateTime.compare(object.updated_at, end_time)} do + PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id}) + |> Oban.insert(unique: [period: @poll_refresh_interval]) + end + end enddiff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex@@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller + alias Pleroma.Hashtag alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ControllerHelper @@ -120,69 +121,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do defp resource_search(:v2, "hashtags", query, options) do tags_path = Endpoint.url() <> "/tag/" - query - |> prepare_tags(options) + Hashtag.search(query, options) |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) end defp resource_search(:v1, "hashtags", query, options) do - prepare_tags(query, options) - end - - defp prepare_tags(query, options) do - tags = - query - |> preprocess_uri_query() - |> String.split(~r/[^#\w]+/u, trim: true) - |> Enum.uniq_by(&String.downcase/1) - - explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end) - - tags = - if Enum.any?(explicit_tags) do - explicit_tags - else - tags - end - - tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end) - - tags = - if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do - add_joined_tag(tags) - else - tags - end - - Pleroma.Pagination.paginate_list(tags, options) - end - - defp add_joined_tag(tags) do - tags - |> Kernel.++([joined_tag(tags)]) - |> Enum.uniq_by(&String.downcase/1) - end - - # If `query` is a URI, returns last component of its path, otherwise returns `query` - defp preprocess_uri_query(query) do - if query =~ ~r/https?:\/\// do - query - |> String.trim_trailing("/") - |> URI.parse() - |> Map.get(:path) - |> String.split("/") - |> Enum.at(-1) - else - query - end - end - - defp joined_tag(tags) do - tags - |> Enum.map(fn tag -> String.capitalize(tag) end) - |> Enum.join() + Hashtag.search(query, options) end defp with_fallback(f, fallback \\ []) do @@ -190,7 +136,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do f.() rescue error -> - Logger.error("#{__MODULE__} search error: #{inspect(error)}") + Logger.error(Exception.format(:error, error, __STACKTRACE__)) fallback end enddiff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.BookmarkFolder + alias Pleroma.Language.Translation alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ScheduledActivity @@ -44,6 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do ] ) + plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate) + plug( OAuthScopesPlug, %{scopes: ["write:statuses"]} @@ -85,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] ) - @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a + @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a plug( RateLimiter, @@ -549,6 +552,44 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end end + @doc "POST /api/v1/statuses/:id/translate" + def translate( + %{ + assigns: %{user: user}, + private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}} + } = conn, + _ + ) do + with %Activity{object: object} <- Activity.get_by_id_with_object(status_id), + {:visibility, visibility} when visibility in ["public", "unlisted"] <- + {:visibility, Visibility.get_visibility(object)}, + {:language, language} when is_binary(language) <- + {:language, Map.get(params, :lang) || user.language}, + {:ok, result} <- + Translation.translate( + object.data["content"], + object.data["language"], + language + ) do + render(conn, "translation.json", result) + else + {:language, nil} -> + render_error(conn, :bad_request, "Language not specified") + + {:visibility, _} -> + render_error(conn, :not_found, "Record not found") + + {:error, :not_found} -> + render_error(conn, :not_found, "Translation service not configured") + + {:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] -> + render_error(conn, :service_unavailable, "Translation service not available") + + _ -> + render_error(conn, :internal_server_error, "Translation failed") + end + end + @doc "GET /api/v1/favourites" def favourites( %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,diff --git a/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex b/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex@@ -0,0 +1,77 @@ +defmodule Pleroma.Web.MastodonAPI.TagController do + @moduledoc "Hashtag routes for mastodon API" + use Pleroma.Web, :controller + + alias Pleroma.Hashtag + alias Pleroma.Pagination + alias Pleroma.User + + import Pleroma.Web.ControllerHelper, + only: [ + add_link_headers: 2 + ] + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + Pleroma.Web.Plugs.OAuthScopesPlug, + %{scopes: ["read"]} when action in [:show] + ) + + plug( + Pleroma.Web.Plugs.OAuthScopesPlug, + %{scopes: ["read:follows"]} when action in [:show_followed] + ) + + plug( + Pleroma.Web.Plugs.OAuthScopesPlug, + %{scopes: ["write:follows"]} when action in [:follow, :unfollow] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation + + def show(conn, %{id: id}) do + with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do + render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user) + else + _ -> conn |> render_error(:not_found, "Hashtag not found") + end + end + + def follow(conn, %{id: id}) do + with %Hashtag{} = hashtag <- Hashtag.get_by_name(id), + %User{} = user <- conn.assigns.user, + {:ok, _} <- + User.follow_hashtag(user, hashtag) do + render(conn, "show.json", tag: hashtag, for_user: user) + else + _ -> render_error(conn, :not_found, "Hashtag not found") + end + end + + def unfollow(conn, %{id: id}) do + with %Hashtag{} = hashtag <- Hashtag.get_by_name(id), + %User{} = user <- conn.assigns.user, + {:ok, _} <- + User.unfollow_hashtag(user, hashtag) do + render(conn, "show.json", tag: hashtag, for_user: user) + else + _ -> render_error(conn, :not_found, "Hashtag not found") + end + end + + def show_followed(conn, params) do + with %{assigns: %{user: %User{} = user}} <- conn do + params = Map.put(params, :id_type, :integer) + + hashtags = + user + |> User.HashtagFollow.followed_hashtags_query() + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(hashtags) + |> render("index.json", tags: hashtags, for_user: user) + end + end +enddiff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex@@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do # GET /api/v1/timelines/home def home(%{assigns: %{user: user}} = conn, params) do + followed_hashtags = + user + |> User.followed_hashtags() + |> Enum.map(& &1.id) + params = params |> Map.put(:type, ["Create", "Announce"]) @@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.put(:announce_filtering_user, user) |> Map.put(:user, user) |> Map.put(:local_only, params[:local]) + |> Map.put(:followed_hashtags, followed_hashtags) |> Map.delete(:local) activities =diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex@@ -168,9 +168,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do UserRelationship.exists?( user_relationships, :endorsement, - target, reading_user, - &User.endorses?(&2, &1) + target, + &User.endorses?(&1, &2) ) } end @@ -219,10 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do avatar = User.avatar_url(user) |> MediaProxy.url() avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true) - avatar_description = image_description(user.avatar) + avatar_description = User.image_description(user.avatar) header = User.banner_url(user) |> MediaProxy.url() header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) - header_description = image_description(user.banner) + header_description = User.image_description(user.banner) following_count = if !user.hide_follows_count or !user.hide_follows or self, @@ -340,6 +340,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) |> maybe_put_mute_expires_at(user, opts[:for], opts) + |> maybe_put_block_expires_at(user, opts[:for], opts) |> maybe_show_birthday(user, opts[:for]) end @@ -349,10 +350,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp username_from_nickname(_), do: nil - defp image_description(%{"name" => name}), do: name - - defp image_description(_), do: "" - defp maybe_put_follow_requests_count( data, %User{id: user_id} = user, @@ -480,6 +477,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do + Map.put( + data, + :block_expires_at, + UserRelationship.get_block_expire_date(target, user) + ) + end + + defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday)diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex@@ -90,6 +90,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do } end + def render("translation_languages.json", _) do + with true <- Pleroma.Language.Translation.configured?(), + {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do + languages + else + _ -> %{} + end + end + defp common_information(instance) do %{ languages: Keyword.get(instance, :languages, ["en"]), @@ -145,7 +154,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do end, "pleroma:get:main/ostatus", "pleroma:group_actors", - "pleroma:bookmark_folders" + "pleroma:bookmark_folders", + if Pleroma.Language.LanguageDetector.configured?() do + "pleroma:language_detection" + end, + "pleroma:block_expiration" ] |> Enum.filter(& &1) end @@ -243,11 +256,27 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }, vapid: %{ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } + }, + translation: %{enabled: Pleroma.Language.Translation.configured?()} }) end defp pleroma_configuration(instance) do + base_urls = %{} + + base_urls = + if Config.get([:media_proxy, :enabled]) do + Map.put(base_urls, :media_proxy, Config.get([:media_proxy, :base_url])) + else + base_urls + end + + base_urls = + case Config.get([Pleroma.Upload, :base_url]) do + nil -> base_urls + url -> Map.put(base_urls, :upload, url) + end + %{ metadata: %{ account_activation_required: Keyword.get(instance, :account_activation_required), @@ -256,7 +285,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do fields_limits: fields_limits(), post_formats: Config.get([:instance, :allowed_post_formats]), birthday_required: Config.get([:instance, :birthday_required]), - birthday_min_age: Config.get([:instance, :birthday_min_age]) + birthday_min_age: Config.get([:instance, :birthday_min_age]), + translation: supported_languages(), + base_urls: base_urls, + markup: markup() }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) @@ -282,4 +314,37 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }) }) end + + defp supported_languages do + enabled = Pleroma.Language.Translation.configured?() + + source_languages = + with true <- enabled, + {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do + languages + else + _ -> nil + end + + target_languages = + with true <- enabled, + {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do + languages + else + _ -> nil + end + + %{ + source_languages: source_languages, + target_languages: target_languages + } + end + + defp markup do + %{ + allow_inline_images: Config.get([:markup, :allow_inline_images]), + allow_headings: Config.get([:markup, :allow_headings]), + allow_tables: Config.get([:markup, :allow_tables]) + } + end enddiff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex@@ -227,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do mentions: mentions, tags: reblogged[:tags] || [], application: build_application(object.data["generator"]), - language: nil, + language: get_language(object), emojis: [], pleroma: %{ local: activity.local, @@ -445,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do mentions: mentions, tags: build_tags(tags), application: build_application(object.data["generator"]), - language: nil, + language: get_language(object), emojis: build_emojis(object.data["emoji"]), pleroma: %{ local: activity.local, @@ -681,6 +681,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end + def render("translation.json", %{ + content: content, + detected_source_language: detected_source_language, + provider: provider + }) do + %{content: content, detected_source_language: detected_source_language, provider: provider} + end + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do object = Object.normalize(activity, fetch: false) @@ -829,6 +837,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do Utils.get_content_type(nil) end + defp get_language(%{data: %{"language" => "und"}}), do: nil + + defp get_language(object), do: object.data["language"] + defp proxied_url(url, page_url_data) do if is_binary(url) do build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()diff --git a/lib/pleroma/web/mastodon_api/views/tag_view.ex b/lib/pleroma/web/mastodon_api/views/tag_view.ex@@ -0,0 +1,25 @@ +defmodule Pleroma.Web.MastodonAPI.TagView do + use Pleroma.Web, :view + alias Pleroma.User + alias Pleroma.Web.Router.Helpers + + def render("index.json", %{tags: tags, for_user: user}) do + safe_render_many(tags, __MODULE__, "show.json", %{for_user: user}) + end + + def render("show.json", %{tag: tag, for_user: user}) do + following = + with %User{} <- user do + User.following_hashtag?(user, tag) + else + _ -> false + end + + %{ + name: tag.name, + url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name), + history: [], + following: following + } + end +enddiff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do # This only prepares the connection and is not in the process yet @impl Phoenix.Socket.Transport def connect(%{params: params} = transport_info) do - with access_token <- Map.get(params, "access_token"), + with access_token <- find_access_token(transport_info), {:ok, user, oauth_token} <- authenticate_request(access_token), {:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do @@ -244,4 +244,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do def handle_error(conn, _reason) do Plug.Conn.send_resp(conn, 404, "Not Found") end + + defp find_access_token(%{ + connect_info: %{sec_websocket_protocol: [token]} + }), + do: token + + defp find_access_token(%{params: %{"access_token" => token}}), do: token + + defp find_access_token(_), do: nil enddiff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex@@ -71,11 +71,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do drop_static_param_and_redirect(conn) content_type == "image/gif" -> - redirect(conn, external: media_proxy_url) + conn + |> put_status(301) + |> redirect(external: media_proxy_url) min_content_length_for_preview() > 0 and content_length > 0 and content_length < min_content_length_for_preview() -> - redirect(conn, external: media_proxy_url) + conn + |> put_status(301) + |> redirect(external: media_proxy_url) true -> handle_preview(content_type, conn, media_proxy_url)diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex@@ -7,6 +7,7 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ + Pleroma.Web.Metadata.Providers.ActivityPub, Pleroma.Web.Metadata.Providers.RelMe, Pleroma.Web.Metadata.Providers.RestrictIndexing | activated_providers()diff --git a/lib/pleroma/web/metadata/providers/activity_pub.ex b/lib/pleroma/web/metadata/providers/activity_pub.ex@@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ActivityPub do + alias Pleroma.Web.Metadata.Providers.Provider + + @behaviour Provider + + @impl Provider + def build_tags(%{object: %{data: %{"id" => object_id}}}) do + [{:link, [rel: "alternate", type: "application/activity+json", href: object_id], []}] + end + + @impl Provider + def build_tags(%{user: user}) do + [{:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []}] + end + + @impl Provider + def build_tags(_), do: [] +enddiff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do @behaviour Provider @impl Provider - def build_tags(%{user: user}) do + def build_tags(%{user: %{local: true} = user}) do [ {:link, [ @@ -20,4 +20,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do ], []} ] end + + @impl Provider + def build_tags(_), do: [] enddiff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex@@ -67,6 +67,9 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do end end + @impl Provider + def build_tags(_), do: [] + defp build_attachments(%{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = @@ -75,10 +78,10 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> - [ - {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []} - | acc - ] + acc ++ + [ + {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []} + ] # Not using preview_url for this. It saves bandwidth, but the image dimensions will # be wrong. We generate it on the fly and have no way to capture or analyze the @@ -86,18 +89,18 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do # in timelines too, but you can get clever with the aspect ratio metadata as a # workaround. "image" -> - [ - {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []}, - {:meta, [property: "og:image:alt", content: attachment["name"]], []} - | acc - ] + (acc ++ + [ + {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []}, + {:meta, [property: "og:image:alt", content: attachment["name"]], []} + ]) |> maybe_add_dimensions(url) "video" -> - [ - {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []} - | acc - ] + (acc ++ + [ + {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []} + ]) |> maybe_add_dimensions(url) |> maybe_add_video_thumbnail(url)diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex@@ -20,6 +20,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do end) end + @impl Provider + def build_tags(_), do: [] + defp append_fields_tag(bio, fields) do fields |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex@@ -44,6 +44,9 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end end + @impl Provider + def build_tags(_), do: [] + defp title_tag(user) do {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []} end @@ -58,13 +61,13 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do Enum.reduce(attachment["url"], [], fn url, acc -> case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> - [ - {:meta, [name: "twitter:card", content: "player"], []}, - {:meta, [name: "twitter:player:width", content: "480"], []}, - {:meta, [name: "twitter:player:height", content: "80"], []}, - {:meta, [name: "twitter:player", content: player_url(id)], []} - | acc - ] + acc ++ + [ + {:meta, [name: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:player:width", content: "480"], []}, + {:meta, [name: "twitter:player:height", content: "80"], []}, + {:meta, [name: "twitter:player", content: player_url(id)], []} + ] # Not using preview_url for this. It saves bandwidth, but the image dimensions will # be wrong. We generate it on the fly and have no way to capture or analyze the @@ -72,16 +75,16 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do # in timelines too, but you can get clever with the aspect ratio metadata as a # workaround. "image" -> - [ - {:meta, [name: "twitter:card", content: "summary_large_image"], []}, - {:meta, + (acc ++ [ - name: "twitter:image", - content: MediaProxy.url(url["href"]) - ], []}, - {:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []} - | acc - ] + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, + {:meta, + [ + name: "twitter:image", + content: MediaProxy.url(url["href"]) + ], []}, + {:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []} + ]) |> maybe_add_dimensions(url) "video" -> @@ -89,17 +92,17 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do height = url["height"] || 480 width = url["width"] || 480 - [ - {:meta, [name: "twitter:card", content: "player"], []}, - {:meta, [name: "twitter:player", content: player_url(id)], []}, - {:meta, [name: "twitter:player:width", content: "#{width}"], []}, - {:meta, [name: "twitter:player:height", content: "#{height}"], []}, - {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])], - []}, - {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]], - []} - | acc - ] + acc ++ + [ + {:meta, [name: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:player", content: player_url(id)], []}, + {:meta, [name: "twitter:player:width", content: "#{width}"], []}, + {:meta, [name: "twitter:player:height", content: "#{height}"], []}, + {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])], + []}, + {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]], + []} + ] _ -> accdiff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex@@ -16,6 +16,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do :import_from_filesystem, :remote, :download, + :download_zip, :create, :update, :delete @@ -113,6 +114,27 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do end end + def download_zip( + %{private: %{open_api_spex: %{body_params: params}}} = conn, + _ + ) do + name = Map.get(params, :name) + + with :ok <- Pack.download_zip(name, params) do + json(conn, "ok") + else + {:error, error} when is_binary(error) -> + conn + |> put_status(:bad_request) + |> json(%{error: error}) + + {:error, _} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Could not process pack"}) + end + end + def download( %{private: %{open_api_spex: %{body_params: %{url: url, name: name} = params}}} = conn, _diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex@@ -13,7 +13,7 @@ defmodule Pleroma.Web.PleromaAPI.InstancesController do def show(conn, _params) do unreachable = - Instances.get_consistently_unreachable() + Instances.get_unreachable() |> Map.new(fn {host, date} -> {host, to_string(date)} end) json(conn, %{"unreachable" => unreachable})diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex@@ -24,6 +24,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation def create(%{assigns: %{user: user}, body_params: params} = conn, _) do + params = + params + |> Map.put_new(:external_link, Map.get(params, :externalLink)) + with {:ok, activity} <- CommonAPI.listen(user, params) do render(conn, "show.json", activity: activity, for: user) elsediff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex@@ -27,8 +27,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do title: object.data["title"] |> HTML.strip_tags(), artist: object.data["artist"] |> HTML.strip_tags(), album: object.data["album"] |> HTML.strip_tags(), - externalLink: object.data["externalLink"], - length: object.data["length"] + external_link: object.data["externalLink"], + length: object.data["length"], + # DEPRECATED + externalLink: object.data["externalLink"] } enddiff --git a/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex b/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex@@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.APClientApiEnabledPlug do + import Plug.Conn + import Phoenix.Controller, only: [text: 2] + + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + @enabled_path [:activitypub, :client_api_enabled] + + def init(options \\ []), do: Map.new(options) + + def call(conn, %{allow_server: true}) do + if @config_impl.get(@enabled_path, false) do + conn + else + conn + |> assign(:user, nil) + |> assign(:token, nil) + end + end + + def call(conn, _) do + if @config_impl.get(@enabled_path, false) do + conn + else + conn + |> put_status(:forbidden) + |> text("C2S not enabled") + |> halt() + end + end +enddiff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex@@ -19,8 +19,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do options end - def call(%{assigns: %{valid_signature: true}} = conn, _opts) do - conn + def call(%{assigns: %{valid_signature: true}} = conn, _opts), do: conn + + # skip for C2S requests from authenticated users + def call(%{assigns: %{user: %Pleroma.User{}}} = conn, _opts) do + if get_format(conn) in ["json", "activity+json"] do + # ensure access token is provided for 2FA + Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end end def call(conn, _opts) dodiff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex@@ -4,6 +4,7 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do require Pleroma.Constants + import Plug.Conn, only: [put_resp_header: 3] @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration. @@ -44,10 +45,31 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do end defp call_static(conn, opts, from) do + # Prevent content-type spoofing by setting content_types: false opts = opts |> Map.put(:from, from) + |> Map.put(:content_types, false) + conn = set_content_type(conn, conn.request_path) + + # Call Plug.Static with our sanitized content-type Plug.Static.call(conn, opts) end + + defp set_content_type(conn, "/emoji/" <> filepath) do + real_mime = MIME.from_path(filepath) + + clean_mime = + Pleroma.Web.Plugs.Utils.get_safe_mime_type(%{allowed_mime_types: ["image"]}, real_mime) + + put_resp_header(conn, "content-type", clean_mime) + end + + defp set_content_type(conn, filepath) do + real_mime = MIME.from_path(filepath) + put_resp_header(conn, "content-type", real_mime) + end end + +# I think this needs to be uncleaned except for emoji.diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do require Logger alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.Utils @behaviour Plug # no slashes @@ -28,7 +29,9 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do |> Keyword.put(:at, "/__unconfigured_media_plug") |> Plug.Static.init() - %{static_plug_opts: static_plug_opts} + allowed_mime_types = Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types]) + + %{static_plug_opts: static_plug_opts, allowed_mime_types: allowed_mime_types} end def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do @@ -69,13 +72,23 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do defp media_is_banned(_, _), do: false + defp set_content_type(conn, opts, filepath) do + real_mime = MIME.from_path(filepath) + clean_mime = Utils.get_safe_mime_type(opts, real_mime) + put_resp_header(conn, "content-type", clean_mime) + end + defp get_media(conn, {:static_dir, directory}, _, opts) do static_opts = Map.get(opts, :static_plug_opts) |> Map.put(:at, [@path]) |> Map.put(:from, directory) + |> Map.put(:content_types, false) - conn = Plug.Static.call(conn, static_opts) + conn = + conn + |> set_content_type(opts, conn.request_path) + |> Plug.Static.call(static_opts) if conn.halted do conndiff --git a/lib/pleroma/web/plugs/utils.ex b/lib/pleroma/web/plugs/utils.ex@@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.Utils do + @moduledoc """ + Some helper functions shared across several plugs + """ + + def get_safe_mime_type(%{allowed_mime_types: allowed_mime_types} = _opts, mime) do + [maintype | _] = String.split(mime, "/", parts: 2) + if maintype in allowed_mime_types, do: mime, else: "application/octet-stream" + end +enddiff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex@@ -20,7 +20,7 @@ defmodule Pleroma.Web.Push do end def vapid_config do - Application.get_env(:web_push_encryption, :vapid_details, nil) + Application.get_env(:web_push_encryption, :vapid_details, []) end def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config())diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex@@ -54,7 +54,10 @@ defmodule Pleroma.Web.RichMedia.Card do @spec get_by_url(String.t() | nil) :: t() | nil | :error def get_by_url(url) when is_binary(url) do - if @config_impl.get([:rich_media, :enabled]) do + host = URI.parse(url).host + + with true <- @config_impl.get([:rich_media, :enabled]), + true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do url_hash = url_to_hash(url) @cachex.fetch!(:rich_media_cache, url_hash, fn _ -> @@ -69,7 +72,7 @@ defmodule Pleroma.Web.RichMedia.Card do end end) else - :error + false -> :error end end @@ -77,7 +80,10 @@ defmodule Pleroma.Web.RichMedia.Card do @spec get_or_backfill_by_url(String.t(), keyword()) :: t() | nil def get_or_backfill_by_url(url, opts \\ []) do - if @config_impl.get([:rich_media, :enabled]) do + host = URI.parse(url).host + + with true <- @config_impl.get([:rich_media, :enabled]), + true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do case get_by_url(url) do %__MODULE__{} = card -> card @@ -94,7 +100,7 @@ defmodule Pleroma.Web.RichMedia.Card do nil end else - nil + false -> nil end enddiff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex@@ -4,6 +4,7 @@ defmodule Pleroma.Web.RichMedia.Parser do alias Pleroma.Web.RichMedia.Helpers + import Pleroma.Web.Metadata.Utils, only: [scrub_html_and_truncate: 2] require Logger @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) @@ -63,8 +64,20 @@ defmodule Pleroma.Web.RichMedia.Parser do not match?({:ok, _}, Jason.encode(%{key => val})) end) |> Map.new() + |> truncate_title() + |> truncate_desc() end + defp truncate_title(%{"title" => title} = data) when is_binary(title), + do: %{data | "title" => scrub_html_and_truncate(title, 120)} + + defp truncate_title(data), do: data + + defp truncate_desc(%{"description" => desc} = data) when is_binary(desc), + do: %{data | "description" => scrub_html_and_truncate(desc, 200)} + + defp truncate_desc(data), do: data + @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex@@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do |> Enum.reduce(data, fn el, acc -> attributes = normalize_attributes(el, prefix, key_name, value_name) - Map.merge(acc, attributes) + Map.merge(attributes, acc) end) |> maybe_put_title(html) enddiff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex@@ -11,5 +11,16 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do |> MetaTagsParser.parse(html, "og", "property") |> MetaTagsParser.parse(html, "twitter", "name") |> MetaTagsParser.parse(html, "twitter", "property") + |> filter_tags() + end + + defp filter_tags(tags) do + Map.filter(tags, fn {k, _v} -> + cond do + k in ["card", "description", "image", "title", "ttl", "type", "url"] -> true + String.starts_with?(k, "image:") -> true + true -> false + end + end) end enddiff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex@@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do end pipeline :well_known do - plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"]) + plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"]) end pipeline :config do @@ -466,6 +466,7 @@ defmodule Pleroma.Web.Router do get("/import", EmojiPackController, :import_from_filesystem) get("/remote", EmojiPackController, :remote) post("/download", EmojiPackController, :download) + post("/download_zip", EmojiPackController, :download_zip) post("/files", EmojiFileController, :create) patch("/files", EmojiFileController, :update) @@ -740,6 +741,7 @@ defmodule Pleroma.Web.Router do post("/statuses/:id/unbookmark", StatusController, :unbookmark) post("/statuses/:id/mute", StatusController, :mute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation) + post("/statuses/:id/translate", StatusController, :translate) post("/push/subscription", SubscriptionController, :create) get("/push/subscription", SubscriptionController, :show) @@ -755,6 +757,11 @@ defmodule Pleroma.Web.Router do get("/announcements", AnnouncementController, :index) post("/announcements/:id/dismiss", AnnouncementController, :mark_read) + + get("/tags/:id", TagController, :show) + post("/tags/:id/follow", TagController, :follow) + post("/tags/:id/unfollow", TagController, :unfollow) + get("/followed_tags", TagController, :show_followed) end scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -782,6 +789,7 @@ defmodule Pleroma.Web.Router do get("/instance", InstanceController, :show) get("/instance/peers", InstanceController, :peers) get("/instance/rules", InstanceController, :rules) + get("/instance/translation_languages", InstanceController, :translation_languages) get("/statuses", StatusController, :index) get("/statuses/:id", StatusController, :show) @@ -902,9 +910,20 @@ defmodule Pleroma.Web.Router do # Client to Server (C2S) AP interactions pipeline :activitypub_client do plug(:ap_service_actor) + plug(Pleroma.Web.Plugs.APClientApiEnabledPlug) + plug(:fetch_session) + plug(:authenticate) + plug(:after_auth) + end + + # AP interactions used by both S2S and C2S + pipeline :activitypub_server_or_client do + plug(:ap_service_actor) plug(:fetch_session) plug(:authenticate) + plug(Pleroma.Web.Plugs.APClientApiEnabledPlug, allow_server: true) plug(:after_auth) + plug(:http_signature) end scope "/", Pleroma.Web.ActivityPub do @@ -913,11 +932,15 @@ defmodule Pleroma.Web.Router do get("/api/ap/whoami", ActivityPubController, :whoami) get("/users/:nickname/inbox", ActivityPubController, :read_inbox) - get("/users/:nickname/outbox", ActivityPubController, :outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) + end + + scope "/", Pleroma.Web.ActivityPub do + pipe_through([:activitypub_server_or_client]) + + get("/users/:nickname/outbox", ActivityPubController, :outbox) - # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`: get("/users/:nickname/followers", ActivityPubController, :followers) get("/users/:nickname/following", ActivityPubController, :following) get("/users/:nickname/collections/featured", ActivityPubController, :pinned)diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex@@ -19,6 +19,7 @@ defmodule Pleroma.Web.Streamer do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.StreamerView + require Pleroma.Constants @registry Pleroma.Web.StreamerRegistry @@ -305,7 +306,17 @@ defmodule Pleroma.Web.Streamer do User.get_recipients_from_activity(item) |> Enum.map(fn %{id: id} -> "user:#{id}" end) - Enum.each(recipient_topics, fn topic -> + hashtag_recipients = + if Pleroma.Constants.as_public() in item.recipients do + Pleroma.Hashtag.get_recipients_for_activity(item) + |> Enum.map(fn id -> "user:#{id}" end) + else + [] + end + + all_recipients = Enum.uniq(recipient_topics ++ hashtag_recipients) + + Enum.each(all_recipients, fn topic -> push_to_socket(topic, item) end) enddiff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex@@ -231,8 +231,8 @@ </div> <%= for %{data: mention, object: object, from: from} <- @mentions do %> - <%# mention START %> - <%# user card START %> + <% # mention START %> + <% # user card START %> <div style="background-color:transparent;"> <div class="block-grid mixed-two-up no-stack" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -291,7 +291,7 @@ </div> </div> </div> - <%# user card END %> + <% # user card END %> <div style="background-color:transparent;"> <div class="block-grid" @@ -333,12 +333,12 @@ </div> </div> </div> - <%# mention END %> + <% # mention END %> <% end %> <%= if @followers != [] do %> - <%# new followers header START %> + <% # new followers header START %> <div style="background-color:transparent;"> <div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -397,10 +397,10 @@ </div> </div> </div> - <%# new followers header END %> + <% # new followers header END %> <%= for %{data: follow, from: from} <- @followers do %> - <%# user card START %> + <% # user card START %> <div style="background-color:transparent;"> <div class="block-grid mixed-two-up no-stack" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -459,13 +459,13 @@ </div> </div> </div> - <%# user card END %> + <% # user card END %> <% end %> <% end %> - <%# divider start %> + <% # divider start %> <div style="background-color:transparent;"> <div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -514,7 +514,7 @@ </div> </div> - <%# divider end %> + <% # divider end %> <div style="background-color:transparent;">diff --git a/lib/pleroma/web/templates/email/new_users_digest.html.eex b/lib/pleroma/web/templates/email/new_users_digest.html.eex@@ -1,5 +1,5 @@ <%= for {user, total_statuses, latest_status} <- @users_and_statuses do %> - <%# user card START %> + <% # user card START %> <div style="background-color:transparent;"> <div class="block-grid mixed-two-up no-stack" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -60,7 +60,7 @@ </div> </div> </div> - <%# user card END %> + <% # user card END %> <%= if latest_status do %> <div style="background-color:transparent;"> @@ -104,7 +104,7 @@ </div> </div> <% end %> - <%# divider start %> + <% # divider start %> <div style="background-color:transparent;"> <div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -153,6 +153,6 @@ </div> </div> - <%# divider end %> - <%# user card END %> + <% # divider end %> + <% # user card END %> <% end %>diff --git a/lib/pleroma/web/templates/layout/email_styled.html.eex b/lib/pleroma/web/templates/layout/email_styled.html.eex@@ -111,7 +111,7 @@ <td style="word-break: break-word; vertical-align: top;" valign="top"> <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]--> - <%# header %> + <% # header %> <div style="background-color:transparent;"> <div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;"> @@ -145,7 +145,7 @@ </div> - <%# title %> + <% # title %> <%= if @title do %> <div style="background-color:transparent;"> <div class="block-grid"diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex@@ -2,7 +2,7 @@ <%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %> <div class="scopes"> <%= for scope <- @available_scopes do %> - <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> + <% # Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> <%= if scope in @scopes do %> <div class="scope"> <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex@@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Healthcheck alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.WebFinger @@ -195,19 +196,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn, _ ) do - case CommonAPI.Utils.confirm_current_password(user, body_params.password) do - {:ok, user} -> - with {:ok, _user} <- - User.reset_password(user, %{ - password: body_params.new_password, - password_confirmation: body_params.new_password_confirmation - }) do - json(conn, %{status: "success"}) - else - {:error, changeset} -> - {_, {error, _}} = Enum.at(changeset.errors, 0) - json(conn, %{error: "New password #{error}."}) - end + with {:ok, %User{}} <- + Authenticator.change_password( + user, + body_params.password, + body_params.new_password, + body_params.new_password_confirmation + ) do + json(conn, %{status: "success"}) + else + {:error, %Ecto.Changeset{} = changeset} -> + {_, {error, _}} = Enum.at(changeset.errors, 0) + json(conn, %{error: "New password #{error}."}) + + {:error, :password_confirmation} -> + json(conn, %{error: "New password does not match confirmation."}) {:error, msg} -> json(conn, %{error: msg})diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex@@ -15,7 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TokenView do %{ id: token_entry.id, valid_until: token_entry.valid_until, - app_name: token_entry.app.client_name + app_name: token_entry.app.client_name, + scopes: token_entry.scopes } end enddiff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex@@ -35,9 +35,9 @@ defmodule Pleroma.Web.WebFinger do regex = if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do - ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/ + ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})$/ else - ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ + ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}$/ end with %{"username" => username} <- Regex.named_captures(regex, resource),diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex@@ -41,5 +41,10 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do end end + # Default to JSON when no format is specified or format is not recognized + def webfinger(%{assigns: %{format: _format}} = conn, %{"resource" => _resource} = params) do + webfinger(put_in(conn.assigns.format, "json"), params) + end + def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request") enddiff --git a/lib/pleroma/workers/delete_worker.ex b/lib/pleroma/workers/delete_worker.ex@@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.DeleteWorker do - alias Pleroma.Instances.Instance alias Pleroma.User use Oban.Worker, queue: :slow @@ -15,7 +14,27 @@ defmodule Pleroma.Workers.DeleteWorker do end def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do - Instance.perform(:delete_instance, host) + # Schedule the per-user deletion jobs + Pleroma.Repo.transaction(fn -> + User.Query.build(%{nickname: "@#{host}"}) + |> Pleroma.Repo.all() + |> Enum.each(fn user -> + %{"op" => "delete_user", "user_id" => user.id} + |> __MODULE__.new() + |> Oban.insert() + end) + + # Delete the instance from the Instances table + case Pleroma.Repo.get_by(Pleroma.Instances.Instance, host: host) do + nil -> :ok + instance -> Pleroma.Repo.delete(instance) + end + + # Delete any pending ReachabilityWorker jobs for this domain + Pleroma.Workers.ReachabilityWorker.delete_jobs_for_host(host) + + :ok + end) end @impl truediff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex@@ -5,9 +5,13 @@ defmodule Pleroma.Workers.MuteExpireWorker do use Oban.Worker, queue: :background + alias Pleroma.User + @impl true - def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do - Pleroma.User.unmute(muter_id, mutee_id) + def perform(%Job{ + args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id} + }) do + User.unmute(muter_id, mutee_id) :ok end @@ -18,6 +22,17 @@ defmodule Pleroma.Workers.MuteExpireWorker do :ok end + def perform(%Job{ + args: %{"op" => "unblock_user", "blocker_id" => blocker_id, "blocked_id" => blocked_id} + }) do + Pleroma.Web.CommonAPI.unblock( + User.get_cached_by_id(blocked_id), + User.get_cached_by_id(blocker_id) + ) + + :ok + end + @impl true def timeout(_job), do: :timer.seconds(5) enddiff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex@@ -11,27 +11,46 @@ defmodule Pleroma.Workers.PollWorker do alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Fetcher + + @stream_out_impl Pleroma.Config.get( + [__MODULE__, :stream_out], + Pleroma.Web.ActivityPub.ActivityPub + ) @impl true def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do - with %Activity{} = activity <- find_poll_activity(activity_id), + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)}, {:ok, notifications} <- Notification.create_poll_notifications(activity) do + unless activity.local do + # Schedule a final refresh + __MODULE__.new(%{"op" => "refresh", "activity_id" => activity_id}) + |> Oban.insert() + end + Notification.stream(notifications) else - {:error, :poll_activity_not_found} = e -> {:cancel, e} + {:activity, nil} -> {:cancel, :poll_activity_not_found} e -> {:error, e} end end - @impl true - def timeout(_job), do: :timer.seconds(5) + def perform(%Job{args: %{"op" => "refresh", "activity_id" => activity_id}}) do + with {_, %Activity{object: object}} <- + {:activity, Activity.get_by_id_with_object(activity_id)}, + {_, {:ok, _object}} <- {:refetch, Fetcher.refetch_object(object)} do + stream_update(activity_id) - defp find_poll_activity(activity_id) do - with nil <- Activity.get_by_id(activity_id) do - {:error, :poll_activity_not_found} + :ok + else + {:activity, nil} -> {:cancel, :poll_activity_not_found} + {:refetch, _} = e -> {:cancel, e} end end + @impl true + def timeout(_job), do: :timer.seconds(5) + def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <- Object.normalize(activity), @@ -49,4 +68,10 @@ defmodule Pleroma.Workers.PollWorker do end def schedule_poll_end(activity), do: {:error, activity} + + defp stream_update(activity_id) do + Activity.get_by_id(activity_id) + |> Activity.normalize() + |> @stream_out_impl.stream_out() + end enddiff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex@@ -4,9 +4,10 @@ defmodule Pleroma.Workers.PublisherWorker do alias Pleroma.Activity + alias Pleroma.Instances alias Pleroma.Web.Federator - use Oban.Worker, queue: :federator_outgoing, max_attempts: 5 + use Oban.Worker, queue: :federator_outgoing, max_attempts: 13 @impl true def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do @@ -14,9 +15,30 @@ defmodule Pleroma.Workers.PublisherWorker do Federator.perform(:publish, activity) end - def perform(%Job{args: %{"op" => "publish_one", "params" => params}}) do + def perform(%Job{args: %{"op" => "publish_one", "params" => params}} = job) do params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end) - Federator.perform(:publish_one, params) + + # Cancel / skip the job if this server believed to be unreachable now + if not Instances.reachable?(params.inbox) do + {:cancel, :unreachable} + else + case Federator.perform(:publish_one, params) do + {:ok, _} -> + :ok + + {:error, _} = error -> + # Only mark as unreachable on final failure + if job.attempt == job.max_attempts do + Instances.set_unreachable(params.inbox) + end + + error + + error -> + # Unexpected error, may have been client side + error + end + end end @impl truediff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex@@ -0,0 +1,116 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ReachabilityWorker do + use Oban.Worker, + queue: :background, + max_attempts: 1, + unique: [period: :infinity, states: [:available, :scheduled], keys: [:domain]] + + alias Pleroma.HTTP + alias Pleroma.Instances + + import Ecto.Query + + @impl true + def perform(%Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}}) do + case check_reachability(domain) do + :ok -> + Instances.set_reachable("https://#{domain}") + :ok + + {:error, _} = error -> + handle_failed_attempt(domain, phase, attempt) + error + end + end + + # New jobs enter here and are immediately re-scheduled for the first phase + @impl true + def perform(%Oban.Job{args: %{"domain" => domain}}) do + scheduled_at = DateTime.add(DateTime.utc_now(), 60, :second) + + %{ + "domain" => domain, + "phase" => "phase_1min", + "attempt" => 1 + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + + :ok + end + + @impl true + def timeout(_job), do: :timer.seconds(5) + + @doc "Deletes scheduled jobs to check reachability for specified instance" + def delete_jobs_for_host(host) do + Oban.Job + |> where(worker: "Pleroma.Workers.ReachabilityWorker") + |> where([j], j.args["domain"] == ^host) + |> Oban.delete_all_jobs() + end + + defp check_reachability(domain) do + case HTTP.get("https://#{domain}/") do + {:ok, %{status: status}} when status in 200..299 -> + :ok + + {:ok, %{status: _status}} -> + {:error, :unreachable} + + {:error, _} = error -> + error + end + end + + defp handle_failed_attempt(_domain, "final", _attempt), do: :ok + + defp handle_failed_attempt(domain, phase, attempt) do + {interval_minutes, max_attempts, next_phase} = get_phase_config(phase) + + if attempt >= max_attempts do + # Move to next phase + schedule_next_phase(domain, next_phase) + else + # Retry same phase with incremented attempt + schedule_retry(domain, phase, attempt + 1, interval_minutes) + end + end + + defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"} + defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"} + defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"} + defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"} + defp get_phase_config("phase_24hour"), do: {1440, 4, "final"} + defp get_phase_config("final"), do: {nil, 0, nil} + + defp schedule_next_phase(_domain, "final"), do: :ok + + defp schedule_next_phase(domain, next_phase) do + {interval_minutes, _max_attempts, _next_phase} = get_phase_config(next_phase) + scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second) + + %{ + "domain" => domain, + "phase" => next_phase, + "attempt" => 1 + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + end + + def schedule_retry(domain, phase, attempt, interval_minutes) do + scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second) + + %{ + "domain" => domain, + "phase" => phase, + "attempt" => attempt + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + end +enddiff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex@@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorker do + alias Pleroma.Instances alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.Federator @@ -37,6 +38,11 @@ defmodule Pleroma.Workers.ReceiverWorker do {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + {:ok, res} else e -> process_errors(e) @@ -45,6 +51,11 @@ defmodule Pleroma.Workers.ReceiverWorker do def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + {:ok, res} else e -> process_errors(e)diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex@@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.RemoteFetcherWorker do + alias Pleroma.Instances alias Pleroma.Object.Fetcher use Oban.Worker, queue: :background, unique: [period: :infinity] @@ -11,6 +12,11 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do {:ok, _object} -> + unless Instances.reachable?(id) do + # Mark the server as reachable since we successfully fetched an object + Instances.set_reachable(id) + end + :ok {:allowed_depth, false} ->diff --git a/mix.exs b/mix.exs@@ -4,8 +4,8 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.7.0"), - elixir: "~> 1.13", + version: version("2.9.1"), + elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), elixirc_options: [warnings_as_errors: warnings_as_errors(), prune_code_paths: false], @@ -37,22 +37,13 @@ defmodule Pleroma.Mixfile do pleroma: [ include_executables_for: [:unix], applications: [ex_syslogger: :load, syslog: :load, eldap: :transient], - steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1], + steps: [:assemble, ©_files/1, ©_nginx_config/1], config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}] ] ] ] end - def put_otp_version(%{path: target_path} = release) do - File.write!( - Path.join([target_path, "OTP_VERSION"]), - Pleroma.OTPVersion.version() - ) - - release - end - def copy_files(%{path: target_path} = release) do File.cp_r!("./rel/files", target_path) release @@ -132,22 +123,23 @@ defmodule Pleroma.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.7.3"}, + {:phoenix, + git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.10"}, {:ecto_enum, "~> 1.4"}, - {:postgrex, ">= 0.0.0"}, + {:postgrex, ">= 0.20.0"}, {:phoenix_html, "~> 3.3"}, {:phoenix_live_view, "~> 0.19.0"}, {:phoenix_live_dashboard, "~> 0.8.0"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:tzdata, "~> 1.0.3"}, - {:plug_cowboy, "~> 2.5"}, - {:oban, "~> 2.18.0"}, {:oban_plugins_lazarus, git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, + {:plug_cowboy, "~> 2.7"}, + {:oban, "~> 2.19.0"}, {:gettext, "~> 0.20"}, {:bcrypt_elixir, "~> 2.2"}, {:trailing_format_plug, "~> 0.0.7"}, @@ -156,9 +148,9 @@ defmodule Pleroma.Mixfile do {:calendar, "~> 1.0"}, {:cachex, "~> 3.2"}, {:tesla, "~> 1.11"}, - {:castore, "~> 0.1"}, - {:cowlib, "~> 2.9", override: true}, - {:gun, "~> 2.0.0-rc.1", override: true}, + {:castore, "~> 1.0"}, + {:cowlib, "~> 2.15"}, + {:gun, "~> 2.2"}, {:finch, "~> 0.15"}, {:jason, "~> 1.2"}, {:mogrify, "~> 0.9.0", override: "true"}, @@ -172,6 +164,8 @@ defmodule Pleroma.Mixfile do {:swoosh, "~> 1.16.9"}, {:phoenix_swoosh, "~> 1.1"}, {:gen_smtp, "~> 0.13"}, + {:mua, "~> 0.2.0"}, + {:mail, "~> 0.3.0"}, {:ex_syslogger, "~> 1.4"}, {:floki, "~> 0.35"}, {:timex, "~> 3.6"}, @@ -193,11 +187,11 @@ defmodule Pleroma.Mixfile do ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", - ref: "6630c42aaaab124e697b4e513190c89d8b64e410"}, + ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, {:majic, "~> 1.0"}, {:open_api_spex, "~> 3.16"}, - {:ecto_psql_extras, "~> 0.6"}, + {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.26.0"}, {:elixir_make, "~> 0.7.7", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, @@ -213,7 +207,7 @@ defmodule Pleroma.Mixfile do {:poison, "~> 3.0", only: :test}, {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_machina, "~> 2.4", only: :test}, - {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.5", only: :test}, {:covertool, "~> 2.0", only: :test}, {:hackney, "~> 1.18.0", override: true}, @@ -236,7 +230,7 @@ defmodule Pleroma.Mixfile do "ecto.rollback": ["pleroma.ecto.rollback"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"], + test: ["ecto.create --quiet", "ecto.migrate", "test --warnings-as-errors"], docs: ["pleroma.docs", "docs"], analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"], copyright: &add_copyright/1,diff --git a/mix.lock b/mix.lock@@ -1,121 +1,124 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, - "argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"}, - "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, + "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, + "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, - "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, + "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, "blurhash": {:hex, :rinpatch_blurhash, "0.1.0", "01a888b0f5f1f382ab52e4396f01831cbe8486ea5828604c90f4dac533d39a4b", [:mix], [{:mogrify, "~> 0.8.0", [hex: :mogrify, repo: "hexpm", optional: true]}], "hexpm", "19911a5dcbb0acb9710169a72f702bce6cb048822b12de566ccd82b2cc42b907"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, - "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, - "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]}, - "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, - "cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"}, + "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, + "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]}, + "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"}, - "covertool": {:hex, :covertool, "2.0.6", "4a291b4e3449025b0595d8f44c8d7635d4f48f033be2ce88d22a329f36f94a91", [:rebar3], [], "hexpm", "5db3fcd82180d8ea4ad857d4d1ab21a8d31b5aee0d60d2f6c0f9e25a411d1e21"}, - "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"}, + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.15", "0fc29dbae0e444a29bd6abeee4cf3c4c037e692a272478a234a1cc765077dbb1", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "b6127f3a5c6fc3d84895e4768cc7c199f22b48b67d6c99b13fbf4a374e73f039"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.8", "aa02529c97f69aed5722899f5dc6360128735a92dd169f23c5d50b1f7fdede08", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "04c63d92b141723ad6fed2e60a4b461ca00b3594d16df47bbc48f1f4534f2c49"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, - "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"}, - "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, - "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, + "ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"}, + "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, + "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "exile": {:hex, :exile, "0.10.0", "b69e2d27a9af670b0f0a0898addca0eda78f6f5ba95ccfbc9bc6ccdd04925436", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c62ee8fee565b5ac4a898d0dcd58d2b04fb5eec1655af1ddcc9eb582c6732c33"}, "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, - "fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"}, + "fast_html": {:hex, :fast_html, "2.5.0", "23578c1c1fa03db6a3786d78218b2d944df34b0fc3729e72de912f390913c80f", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "69eb46ed98a5d9cca1ccd4a5ac94ce5dd626fc29513fbaa0a16cd8b2da67ae3e"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, - "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, - "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, - "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"}, - "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, + "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, + "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, - "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, + "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, + "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, - "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"}, + "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, - "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, + "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, - "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, + "mua": {:hex, :mua, "0.2.4", "a9172ab0a1ac8732cf2699d739ceac3febcb9b4ffc540260ad2e32c0b6632af9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "e7e4dacd5ad65f13e3542772e74a159c00bd2d5579e729e9bb72d2c73a266fb7"}, "multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, - "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "2.18.2", "583e78965ee15263ac968e38c983bad169ae55eadaa8e1e39912562badff93ba", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9dd25fd35883a91ed995e9fe516e479344d3a8623dfe2b8c3fc8e5be0228ec3a"}, + "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, - "open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"}, + "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, - "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix": {:git, "https://github.com/feld/phoenix", "fb6dc76c657422e49600896c64aab4253fceaef6", [branch: "v1.7.14-websocket-headers"]}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, + "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, - "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5 or ~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"}, + "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"}, "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex.git", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]}, @@ -123,33 +126,33 @@ "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, "rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"}, - "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, + "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, - "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, + "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, + "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, + "swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, - "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"}, + "table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"}, "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, - "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.0", "b583c3f18508f5c5561b674d16cf5d9afd2ea3c04505b7d92baaeac93c1b8260", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "9cba950e1c4733468efbe3f821841f34ac05d28e7af7798622f88ecdbbe63ea3"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, - "tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"}, - "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, + "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"}, - "ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, - "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 1.0 or ~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.2 or ~> 0.1.4", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8 or ~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"}, + "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, }diff --git a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po@@ -3,14 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-22 02:09+0300\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"PO-Revision-Date: 2024-10-13 21:03+0000\n" +"Last-Translator: Codimp <contact@lithio.fr>\n" +"Language-Team: French <https://translate.pleroma.social/projects/pleroma/" +"pleroma-backend-domain-config_descriptions/fr/>\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Translate Toolkit 3.7.2\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.13.1\n" ## This file is a PO Template file. ## @@ -21,7 +23,6 @@ msgstr "" ## Run "mix gettext.extract" to bring this file up to ## date. Leave "msgstr"s empty as changing them here has no ## effect: edit them in PO (.po) files instead. - #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :esshd" @@ -32,25 +33,30 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :logger" msgid "Logger-related settings" -msgstr "" +msgstr "Paramètres liés à la journalisation" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :mime" msgid "Mime Types settings" -msgstr "" +msgstr "Paramètres des types Mime" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)" msgstr "" +"Permet de configurer un jeton qui peut être utilisé pour authentifier les " +"requêtes avec des privilèges administrateurs sans utiliser un jeton de " +"compte utilisateur standard. Pour l'utiliser, ajoutez le paramètre " +"`admin_token`aux requêtes. (Vous devriez utiliser l'authentification HTTP " +"Basic ou OAuth à la place si vous le pouvez)" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Authenticator" -msgstr "" +msgstr "Authentifieur" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -62,7 +68,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :cors_plug" msgid "CORS plug config" -msgstr "" +msgstr "Configuration du plug CORS" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -74,25 +80,25 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :logger" msgid "Logger" -msgstr "" +msgstr "Journaliseur" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :mime" msgid "Mime Types" -msgstr "" +msgstr "Types Mime" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Admin Token" -msgstr "" +msgstr "Jeton Administrateur Pleroma" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Authenticator" -msgstr "" +msgstr "Authentifieur Pleroma" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -104,103 +110,111 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :logger-:console" msgid "Console logger settings" -msgstr "" +msgstr "Paramètres de journalisation de la console" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger" msgid "ExSyslogger-related settings" -msgstr "" +msgstr "Paramètres liés à ExSyslogger" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub" msgid "ActivityPub-related settings" -msgstr "" +msgstr "Paramètres liés à ActivityPub" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets" msgid "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend" msgstr "" +"Cette section configure les annexes (assets) à utiliser avec divers " +"frontaux. La seule option est actuellement liée au mascottes du frontal " +"mastodon" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth" msgid "Authentication / authorization settings" -msgstr "" +msgstr "Paramètres d'authentification/autorisations" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool" msgid "Advanced settings for `Gun` connections pool" -msgstr "" +msgstr "Paramètres avancés pour le bac (pool) de connexions `Gun`" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications" msgid "Email notifications settings" -msgstr "" +msgstr "Paramètres de notification par email" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:features" msgid "Customizable features" -msgstr "" +msgstr "Fonctionnalités personnalisables" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed" msgid "Configure feed rendering" -msgstr "" +msgstr "Configurer le rendu des flux" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations" msgid "This form can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for pleroma_fe are configured. If you want to add your own configuration your settings all fields must be complete." msgstr "" +"Ce formulaire peut être utilisé pour configurer une liste de clés (keyword) " +"qui contiennent les données de configuration pour tout types de frontaux. " +"Par défaut, les paramètres pour pleroma_fe sont configurés. Si vous voulez " +"ajouter vos propres paramètres de configurations, tout les champs doivent " +"être remplis." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends" msgid "Installed frontends management" -msgstr "" +msgstr "Gestion des frontaux installés" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher" msgid "Gopher settings" -msgstr "" +msgstr "Paramètres Gopher" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools" msgid "Advanced settings for `Hackney` connections pools" -msgstr "" +msgstr "Paramètres avancés pour les bacs (pool) de connexions `Hackney`" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http" msgid "HTTP settings" -msgstr "" +msgstr "Paramètres HTTP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security" msgid "HTTP security settings" -msgstr "" +msgstr "Paramètres de sécurité HTTP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance" msgid "Instance-related settings" -msgstr "" +msgstr "Paramètres liés à l'instance" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instances_favicons" msgid "Control favicons for instances" -msgstr "" +msgstr "Gère les favicons des instances" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -212,151 +226,177 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:majic_pool" msgid "Majic/libmagic configuration" -msgstr "" +msgstr "Configuration de majic/libmagic" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest" msgid "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE." msgstr "" +"Cette section décrit les valeurs spécifique à l'instance du manifeste PWA. " +"Actuellement, cette option ne concerne que MastoFE." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy" msgid "Media preview proxy" -msgstr "" +msgstr "Proxy de prévisualisation média" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy" msgid "Media proxy" -msgstr "" +msgstr "Proxy média" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:modules" msgid "Custom Runtime Modules" -msgstr "" +msgstr "Modules Runtime Personalisés" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf" msgid "General MRF settings" -msgstr "" +msgstr "Paramètres généraux MRF" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_activity_expiration" msgid "Adds automatic expiration to all local activities" -msgstr "" +msgstr "Ajoute une expiration automatique à toutes les activités locales" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_follow_bot" msgid "Automatically follows newly discovered accounts." -msgstr "" +msgstr "Suivre automatiquement les comptes venant d'être découverts." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag" msgid "Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)\n\nNote: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.\n" msgstr "" +"Rejeter, Enlever de TWKN ou marquer comme contenu sensible les messages avec " +"des mots-croisillons (sans mettre le # du début)\n" +"\n" +"Note: cette politique MRF est toujours activée. Si vous voulez la " +"désactiver, vous devez configurer des listes vides.\n" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hellthread" msgid "Block messages with excessive user mentions" -msgstr "" +msgstr "Bloquer les messages avec un nombre excessif de mentions" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword" msgid "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html)." msgstr "" +"Rejeter ou remplacer les mots des messages qui correspondent à un mot clef " +"ou à une [expression rationnelle (Regex)](https://hexdocs.pm/elixir/Regex." +"html)." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_mention" msgid "Block messages which mention a specific user" -msgstr "" +msgstr "Bloquer les messages mentionnant un utilisateur particulier" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_normalize_markup" msgid "MRF NormalizeMarkup settings. Scrub configured hypertext markup." msgstr "" +"Paramètres de normalisation MRF. Balaie les balises hypertextes configurées." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_object_age" msgid "Rejects or delists posts based on their timestamp deviance from your server's clock." msgstr "" +"Rejette ou retire des listes les messages selon l'écart entre leur heure et " +"l'horloge de votre serveur." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic" msgid "RejectNonPublic drops posts with non-public visibility settings." msgstr "" +"RejectNonPublic enlève les messages avec des paramètres de visibilité non-" +"publics." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple" msgid "Simple ingress policies" -msgstr "" +msgstr "Politiques simples pour entrants" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji" msgid "Steals emojis from selected instances when it sees them." -msgstr "" +msgstr "Vole les emojis des instances sélectionnées quand il les voit." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_subchain" msgid "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules." msgstr "" +"Cette politique traite les messages à travers un tuyau séparé lorsqu'un " +"message donné correspond à certain critères. Chaque critère est configuré " +"comme une correspondance entre une expression rationnelle et une liste de " +"modules de politiques." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_vocabulary" msgid "Filter messages which belong to certain activity vocabularies" msgstr "" +"Filtrer les messages qui correspondent à certain vocabulaires d'activités" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2" msgid "Configure OAuth 2 provider capabilities" -msgstr "" +msgstr "Configurer les capacités du fournisseur OAuth 2" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools" msgid "Advanced settings for `Gun` workers pools" -msgstr "" +msgstr "Paramètres avancés pour les bacs (pools) de travailleurs `Gun`" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:populate_hashtags_table" msgid "`populate_hashtags_table` background migration settings" -msgstr "" +msgstr "Paramètres de migration en arrière-plan `populate_hashtags_table`" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit" msgid "Rate limit settings. This is an advanced feature enabled only for :authentication by default." msgstr "" +"Paramètres de limites par secondes. C'est une fonctionnalité avancée qui, " +"par défaut, n'est activée que pour :authentication." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated" msgid "Disallow viewing timelines, user profiles and statuses for unauthenticated users." msgstr "" +"Empêche de regarder les flux, les profils utilisateurs et les status pour " +"les utilisateurs non-authentifiés." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media" msgid "If enabled the instance will parse metadata from attached links to generate link previews" msgstr "" +"Si activé, l'instance interprétera les métadonnées des liens joins pour " +"générer les prévisualisations de liens" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -369,6 +409,8 @@ msgstr "" msgctxt "config description at :pleroma-:static_fe" msgid "Render profiles and posts using server-generated HTML that is viewable without using JavaScript" msgstr "" +"Rendre les profils et les status en utilisant du HTML généré par le serveur " +"qui ne nécessitera pas de JavaScript" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -380,7 +422,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:uri_schemes" msgid "URI schemes related settings" -msgstr "" +msgstr "Paramètres liés au schémas d'URI" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-formatdiff --git a/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs b/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs@@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do + use Ecto.Migration + + def change do + create table(:user_follows_hashtag) do + add(:hashtag_id, references(:hashtags)) + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + end + + create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id])) + + create_if_not_exists(index(:user_follows_hashtag, [:hashtag_id])) + end +enddiff --git a/priv/repo/migrations/20240904142434_assign_app_user.exs b/priv/repo/migrations/20240904142434_assign_app_user.exs@@ -1,20 +1,24 @@ defmodule Pleroma.Repo.Migrations.AssignAppUser do use Ecto.Migration + import Ecto.Query + alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Token def up do - Repo.all(Token) - |> Enum.group_by(fn x -> Map.get(x, :app_id) end) - |> Enum.each(fn {_app_id, tokens} -> - token = - Enum.filter(tokens, fn x -> not is_nil(x.user_id) end) - |> List.first() - + Token + |> where([t], not is_nil(t.user_id)) + |> group_by([t], t.app_id) + |> select([t], %{app_id: t.app_id, id: min(t.id)}) + |> order_by(asc: :app_id) + |> Repo.stream() + |> Stream.each(fn %{id: id} -> + token = Token.Query.get_by_id(id) |> Repo.one() App.maybe_update_owner(token) end) + |> Stream.run() end def down, do: :okdiff --git a/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs b/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs@@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.AddActivitiesActorTypeIndex do + use Ecto.Migration + @disable_ddl_transaction true + + def change do + create( + index( + :activities, + ["actor", "(data ->> 'type'::text)", "id DESC NULLS LAST"], + concurrently: true + ) + ) + end +enddiff --git a/rel/vm.args.eex b/rel/vm.args.eex@@ -9,3 +9,8 @@ ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10 + +# Disable wasteful busywait. ++sbwt none ++sbwtdcpu none ++sbwtdio nonediff --git a/test/fixtures/break_analyze.png b/test/fixtures/break_analyze.pngBinary files differ.diff --git a/test/fixtures/friendica-dislike-undo.json b/test/fixtures/friendica-dislike-undo.json@@ -0,0 +1,76 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182/Undo", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": { + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" + }, + "published": "2025-06-12T18:41:25Z", + "signature": { + "created": "2025-06-12T18:44:16Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "2d67847d4bd4b1b83a30d61eac6cdc7ad6b980df06a8b9b97217e1d8f7b6cf20", + "signatureValue": "LnoRMZuQGDvTICkShGBq28ynaj2lF1bViJFGS6n4gKn3IbxPWATHxao43gxWRc+HCTrHNg7quzgaW4+PYM7UVUz3jO+bjNKsN845nijOVdyFrPOXbuaij3KQh2OoHhFJWoV/ZQQTFF0kRK1qT4BwG+P8NqOOKAMv+Cw7ruQH+f2w7uDgcNIbCD1gLcwb6cw7WVe5qu8yMkKqp2kBdqW3RCsI85RmmFgwehDgH5nrX7ER1qbeLWrqy7echwD9/fO3rqAu13xDNyiGZHDT7JB3RUt0AyMm0XCfjbwSQ0n+MkYXgE4asvFz81+iiPCLt+6gePWAFc5odF1FxdySBpSuUOs4p92NzP9OhQ0c0qrqrzYI7aYklY7oMfxjkva+X+0bm3up+2IRJdnZa/pXlmwdcqTpyMr1sgzaexMUNBp3dq7zA51eEaakLDX3i2onXJowfmze3+6XgPAFHYamR+pRNtuEoY4uyYEK3fj5GgwJ4RtFJMYVoEs/Q8h3OgYRcK1FE9UlDjSqbQ7QIRn2Ib4wjgmkeM0vrHIwh/1CtqA/M/6WuYFzCaJBc8O9ykpK9ZMbw64ToQXKf2SqhZsDoyTWRWTO1PXOk1XCAAElUh8/WCyeghvgqLXn0LHov4lmBsHA5iMUcLqBKD3GJIHd+ExrOFxMZs4mBLLGyz0p5joJ3NY=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Undo" +}diff --git a/test/fixtures/friendica-dislike.json b/test/fixtures/friendica-dislike.json@@ -0,0 +1,56 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "signature": { + "created": "2025-06-12T18:47:42Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "84e496f80b09d7a299c5cc89e8cadd13abf621b3a0a321684fa74278b68a6dd8", + "signatureValue": "qe2WxY+j7daIYLRadCctgal6A1s9XgoiMfM/8KjJm15w0sSizYYqruyQ5gS44e+cj5GHc9v5gP2ieod5v7eHAPzlcDI4bfkcyHVapAXTqU67ZebW+v6Q+21IMDgqrkYCv5TbV7LTxltW59dlqovpHE4TEe/M7xLKWJ3vVchRUcWqH9kDmak0nacoqYVAb5E9jYnQhUWPTCfPV82qQpeWQPOZ4iIvPw6rDkSSY5jL6bCogBZblHGpUjXfe/FPlacaCWiTQdoga3yOBXB1RYPw9nh5FI5Xkv/oi+52WmJrECinlD6AL8/BpiYvKz236zy7p/TR4BXlCx9RR/msjOnSabkQ4kmYFrRr80UDCGF+CdkdzLl8K9rSE3PbF1+nEqD7X0GOWn/DdtixqXJw6IR4bh32YW2SlcrSRBvI1p82Mv68BeqRaYqL6FAhKFwLhX5JpXngZ3k0g7rWWxc498voPWnFZDyCTRNxO9VIIUavDDEQ0BdFk6WDb8zx9tsAg8JoK57eVDcFly7tfVQffYiHpve06d8ag1DtzipqguRsURmuqpGNMq28XBTxwtrP2LnXXHKxoYN/YQ9cDnCKclbx7/uKmOVMLkLZlM0wAVoZpm5z2fG4voKqFiGZ1PoiFY2sN4URMArJtygV3PlTX4ASAQrak0ksvEo9msrBUD0Su9c=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" +}diff --git a/test/fixtures/fulmo.html b/test/fixtures/fulmo.html@@ -0,0 +1,151 @@ +<!DOCTYPE html> +<html lang='eo'> + <head> + <meta charset='utf-8'/> + <meta name='author' content='Tirifto'/> + <meta name='generator' content='Pageling'/> + <meta name='viewport' content='width=device-width, + height=device-height, + initial-scale=1.0'/> + <link rel='stylesheet' type='text/css' href='/r/stiloj/tiriftejo.css'/> + <link rel='alternate' type='application/atom+xml' href='/eo/novajhoj.atom'/> + <link rel='icon' size='16x16' type='image/vnd.microsoft.icon' href='/favicon.ico'/> + <link rel='icon' size='128x128' type='image/png' href='/icon.png'/> + <link rel='alternate' hreflang='eo' href='https://tirifto.xwx.moe/eo/rakontoj/fulmo.html'/> + <title>Fulmo</title> + <meta property='og:title' content='Fulmo'/> + <meta property='og:type' content='website'/> + <meta property='og:url' content='https://tirifto.xwx.moe/eo/rakontoj/fulmo.html'/> + <meta property='og:site_name' content='Tiriftejo'/> + <meta property='og:locale' content='eo'/> + <meta property='og:description' content='Pri feoj, kiuj devis ordigi falintan arbon.'/> + <meta property='og:image' content='https://tirifto.xwx.moe/r/ilustrajhoj/pinglordigado.png'/> + <meta property='og:image:alt' content='Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.'/> + <meta property='og:image:height' content='630'/> + <meta property='og:image:width' content='1200'/> + <meta property='og:image' content='https://tirifto.xwx.moe/r/opengraph/eo.png'/> + <meta property='og:image:alt' content='La tirifta okulo ĉirkaŭita de ornamaj steloj kaj la teksto: »Tiriftejo. Esperanto.«'/> + <meta property='og:image:height' content='630'/> + <meta property='og:image:width' content='1200'/> + </head> + <body> + <header id='website-header'> + <nav id='website-navigation'> + <input type='checkbox' id='website-navigation-toggle' + aria-description='Montri ligilojn al ĉefaj paĝoj de la retejo.'/> + <label for='website-navigation-toggle'>Paĝoj</label> + <a href='/eo/verkoj.html'>Verkoj</a> + <a href='/eo/novajhoj.html'>Novaĵoj</a> + <a href='/eo/donacoj.html'>Donacoj</a> + <a href='/eo/prio.html'>Prio</a> + <a href='/eo/amikoj.html'>Amikoj</a> + <a href='/eo/kontakto.html'>Kontakto</a> + + </nav> + <span id='eye' role='img' aria-label=''></span> + <nav id='language-switcher' + aria-roledescription='lingvo-ŝanĝilo'> + <input type='checkbox' id='language-switcher-toggle' + aria-description='Montri ligilojn al tradukoj de tiu ĉi paĝo.'/> + <label for='language-switcher-toggle'>Lingvoj</label> + <a href='fulmo.html' lang='eo' hreflang='eo'><img aria-hidden='true' alt='' src='/r/flagoj/eo.png'/>Esperanto</a> + </nav> + </header> + <div class='bodier'> + <nav id='work-links'> + <a href='.'>Ceteraj rakontoj</a> + <a href='../bildosignoj'>Bildosignoj</a> + <a href='../eseoj'>Eseoj</a> + <a href='../ludoj'>Ludoj</a> + <a href='../poemoj'>Poemoj</a> + <a href='../vortaroj'>Vortaroj</a> + </nav> + <main> + <article> + <header> + <h1>Fulmo</h1> + <p>Skribis Tirifto</p> + <time datetime='2025-01-31'>2025-01-31</time> + </header> + <p>»Kial ĉiam mi? Tio ne justas! Oni kulpigas min, sed ja ne mi kulpas!« La nubofeo lamentis, dum ĝi ordigis restaĵojn de falinta arbo. Plejparto el la pingloj estis brulintaj, kaj el la trunko ankoraŭ leviĝis fumo.</p> + <p>Subite aŭdeblis ekstraj kraketoj deapude. Ĝi rigardis flanken, kaj vidis iun kaŭri apud la arbo, derompi branĉetojn, kaj orde ilin amasigi. Ŝajnis, ke ekde sia rimarkiĝo, la nekonatulo laŭeble kuntiriĝis, kaj strebis labori kiel eble plej silente.</p> + <p>»Saluton…?« La nubofeo stariĝis, alporolante la eston. Tiu kvazaŭ frostiĝis, sed timeme ankaŭ stariĝis.</p> + <p>»S- Saluton…« Ĝi respondis sen kuraĝo rigardi ĝiadirekten. Nun stare, videblis ke ĝi estas verdanta florofeo.</p> + <p>»… kion vi faras tie ĉi?« La nubofeo demandis.</p> + <p>»Nu… tiel kaj tiel… mi ordigas.«</p> + <p>»Ho. Mi ricevis taskon ordigi ĉi tie… se vi povas atendi, vi ne bezonas peni!«</p> + <p>»N- Nu… mi tamen volus…« Parolis la florofeo, plu deturnante la kapon.</p> + <p>»Nu… bone, se vi tion deziras… dankon!« La nubofeo dankis, kaj returniĝis al sia laboro.</p> + <p>Fojfoje ĝi scivole rigardis al sia nova kunlaboranto, kaj fojfoje renkontis similan rigardon de ĝia flanko, kiuokaze ambaŭ rigardoj rapide revenis al la ordigataj pingloj kaj branĉetoj. »(Kial tiom volonte helpi min?)« Pensis al si la nubofeo. »(Ĉu ĝi simple tiom bonkoras? Ĝi ja tre bele floras; eble ankaŭ ĝia koro tiel same belas…)« Kaj vere, ĝiaj surfloroj grandanime malfermis siajn belkolorajn folietojn, kaj bonodoris al mondo.</p> + <figure> + <picture> + <source srcset='/r/ilustrajhoj/pinglordigado.jxl' type='image/jxl'/> + <img src='/r/ilustrajhoj/pinglordigado.png' alt='Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.'/> + </picture> + <figcaption> + Pinglordigado + <details> + <summary>© <time datetime='2025'>2025</time> Tirifto</summary> + <a href='https://artlibre.org/'><img src='/r/permesiloj/lal.svg' class='stamp licence' alt='Emblemo: Permesilo de arto libera'/></a> + </details> + </figcaption> + </figure> + <p>Post iom da tempo, ĉiu feo tralaboris ĝis la trunkomezo, kaj proksimiĝis al la alia feo. Kaj tiam ekpezis sur ili devosento rompi la silenton.</p> + <p>»… kia bela vetero, ĉu ne?« Diris la nubofeo, tuj rimarkonte, ke mallumiĝas, kaj la ĉielo restas kovrita de nuboj.</p> + <p>»Jes ja! Tre nube. Mi ŝatas nubojn!« Respondis la alia entuziasme, sed tuj haltetis kaj deturnis la kapon. Ambaŭ feoj daŭrigis laboron silente, kaj plu proksimiĝis, ĝis tiu preskaŭ estis finita.</p> + <p>»H… H… Ho ne…!« Subite ekdiris la nubofeo urĝe.</p> + <p>»Kio okazas?!«</p> + <p>»T… Tern…!«</p> + <p>»Jen! Tenu!« La florofeo etendis manon kun granda folio. La nubofeo ĝin prenis, kaj tien ternis. Aperis ekfulmo, kaj la cindriĝinta folio disfalis.</p> + <p>»Pardonu… mi ne volis…« Bedaŭris la nubofeo. »Mi ne scias, kial tio ĉiam okazas! Tiom plaĉas al mi promeni tere, sed ĉiuj diras, ke mi maldevus, ĉar ial ĝi ĉiam finiĝas tiel ĉi.« Ĝi montris al la arbo. »Eble ili pravas…«</p> + <p>»Nu…« diris la florofeo bedaŭre, kaj etendis la manon.</p> + <p>»H… H… Ne ree…!«</p> + <p>Ekfulmis. Alia ĵus metita folio cindriĝis en la manoj de la florofeo, time ferminta la okulojn.</p> + <p>»Dankegon… mi tre ŝatas vian helpon! Kaj mi ne… ne…«</p> + <p>Metiĝis. Ekfulmis. Cindriĝis.</p> + <p>»Io tre iritas mian nazon!« Plendis la nubofeo. Poste ĝi rimarkis la florpolvon, kiu disŝutiĝis el la florofeo en la tutan ĉirkaŭaĵon, kaj eĉ tuj antaŭ la nubofeon.</p> + <p>»N- Nu…« Diris la florofeo, honte rigardanta la teron. »… pardonu.«</p> + <footer> + <noscript><p>Ĉu vi ŝatas la verkon? <a href='/eo/donacoj.html'>Subtenu min</a> aŭ kopiu adreson de la verko por diskonigi ĝin!</p></noscript> + <script id='underbuttons'> + /* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 */ + document.getElementById('underbuttons').outerHTML = "<p><a href='/eo/donacoj.html' class='button' target='_blank'>Subtenu min</a> <button onclick='navigator.clipboard.writeText(window.location.href.split(\"\#\")[0]).then(() => window.alert(\"Ligilo al ĉi tiu verko estas kopiita. Sendu ĝin al iu por diskonigi la verkon! 🐱\"))'>Diskonigu la verkon</button></p>" + /* @license-end */ + </script> + <details class='history'> + <summary>Historio</summary> + <dl> + <dt><time datetime='2025-01-31'>2025-01-31</time></dt> + <dd>Unua publikigo.</dd> + </dl> + </details> + <details class='licence' open='details'> + <summary>Permesilo</summary> + <p>Ĉi tiun verkon vi rajtas libere kopii, disdoni, kaj ŝanĝi, laŭ kondiĉoj de la <a href='https://artlibre.org/'>Permesilo de arto libera</a>. (Resume: Vi devas mencii la aŭtoron kaj doni ligilon al la verko. Se vi ŝanĝas la verkon, vi devas laŭeble noti la faritajn ŝanĝojn, ilian daton, kaj eldoni ilin sub la sama permesilo.)</p> + <a href='https://artlibre.org/'><img src='/r/permesiloj/lal.svg' class='stamp licence' alt='Emblemo: Permesilo de arto libera'/></a> + </details> + </footer> + </article> + </main> + </div> + <footer id='website-footer'> + <div class='stamps'> + <a href='https://gnu.org/'> + <img class='stamp' src='/r/retetikedoj/gnu.png' lang='en' alt='GNU'/></a> + <img class='stamp' src='/r/retetikedoj/ihhtus.png' lang='el' alt='ΙΧΘΥΣ'/> + <img class='stamp' src='/r/retetikedoj/be-kind.apng' lang='en' alt='Be kind.'/> + <img class='stamp' src='/r/retetikedoj/kulturo-libera.png' lang='eo' alt='Kulturo libera.'/> + <img class='stamp' src='/r/retetikedoj/discord.png' lang='en' alt='Say ‘no’ to Discord.'/> + <a href='https://xwx.moe/'> + <img class='stamp' src='/r/retetikedoj/xwx-moe.png' alt='xwx.moe'/></a> + <a href='https://mojeek.co.uk' hreflang='en'> + <img class='stamp' src='/r/retetikedoj/mojeek.png' lang='en' alt='Mojeek'/></a> + <a href='https://raku.org/' hreflang='en'> + <img class='stamp' src='/r/retetikedoj/raku.png' alt='Raku'/></a> + <picture> + <source srcset='/r/retetikedoj/jxl.jxl' type='image/jxl'/> + <img class='stamp' src='/r/retetikedoj/jxl.png' alt='JPEG XL'/></picture> + </div> + </footer> + </body> +</html>diff --git a/test/fixtures/mastodon-update-with-likes.json b/test/fixtures/mastodon-update-with-likes.json@@ -0,0 +1,90 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "atomUri": "ostatus:atomUri", + "conversation": "ostatus:conversation", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "ostatus": "http://ostatus.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount" + }, + "https://w3id.org/security/v1" + ], + "actor": "https://pol.social/users/mkljczk", + "cc": ["https://www.w3.org/ns/activitystreams#Public", + "https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk", + "https://pl.fediverse.pl/users/mkljczk", + "https://fedi.kutno.pl/users/mkljczk"], + "id": "https://pol.social/users/mkljczk/statuses/113907871635572263#updates/1738096776", + "object": { + "atomUri": "https://pol.social/users/mkljczk/statuses/113907871635572263", + "attachment": [], + "attributedTo": "https://pol.social/users/mkljczk", + "cc": ["https://www.w3.org/ns/activitystreams#Public", + "https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk", + "https://pl.fediverse.pl/users/mkljczk", + "https://fedi.kutno.pl/users/mkljczk"], + "content": "<p>test</p>", + "contentMap": { + "pl": "<p>test</p>" + }, + "conversation": "https://fedi.kutno.pl/contexts/43c14c70-d3fb-42b4-a36d-4eacfab9695a", + "id": "https://pol.social/users/mkljczk/statuses/113907871635572263", + "inReplyTo": "https://pol.social/users/aemstuz/statuses/113907854282654767", + "inReplyToAtomUri": "https://pol.social/users/aemstuz/statuses/113907854282654767", + "likes": { + "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/likes", + "totalItems": 1, + "type": "Collection" + }, + "published": "2025-01-28T20:29:45Z", + "replies": { + "first": { + "items": [], + "next": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies?only_other_accounts=true&page=true", + "partOf": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies", + "type": "CollectionPage" + }, + "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies", + "type": "Collection" + }, + "sensitive": false, + "shares": { + "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/shares", + "totalItems": 0, + "type": "Collection" + }, + "summary": null, + "tag": [ + { + "href": "https://pol.social/users/aemstuz", + "name": "@aemstuz", + "type": "Mention" + }, + { + "href": "https://gts.mkljczk.pl/users/mkljczk", + "name": "@mkljczk@gts.mkljczk.pl", + "type": "Mention" + }, + { + "href": "https://pl.fediverse.pl/users/mkljczk", + "name": "@mkljczk@fediverse.pl", + "type": "Mention" + }, + { + "href": "https://fedi.kutno.pl/users/mkljczk", + "name": "@mkljczk@fedi.kutno.pl", + "type": "Mention" + } + ], + "to": ["https://pol.social/users/mkljczk/followers"], + "type": "Note", + "updated": "2025-01-28T20:39:36Z", + "url": "https://pol.social/@mkljczk/113907871635572263" + }, + "published": "2025-01-28T20:39:36Z", + "to": ["https://pol.social/users/mkljczk/followers"], + "type": "Update" +}diff --git a/test/fixtures/misskey-custom-emoji-like.json b/test/fixtures/misskey-custom-emoji-like.json@@ -0,0 +1,54 @@ + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_summary": "misskey:_misskey_summary", + "_misskey_votes": "misskey:_misskey_votes", + "backgroundUrl": "sharkey:backgroundUrl", + "discoverable": "toot:discoverable", + "featured": "toot:featured", + "fedibird": "http://fedibird.com/ns#", + "firefish": "https://joinfirefish.org/ns#", + "isCat": "misskey:isCat", + "listenbrainz": "sharkey:listenbrainz", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "misskey": "https://misskey-hub.net/ns#", + "quoteUri": "fedibird:quoteUri", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "sharkey": "https://joinsharkey.org/ns#", + "speakAsCat": "firefish:speakAsCat", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "_misskey_reaction": ":blobwtfnotlikethis:", + "actor": "https://mai.waifuism.life/users/9otxaeemjqy70001", + "content": ":blobwtfnotlikethis:", + "id": "https://mai.waifuism.life/likes/9q2xifhrdnb0001b", + "object": "https://bungle.online/notes/9q2xi2sy4k", + "tag": [ + { + "icon": { + "mediaType": "image/png", + "type": "Image", + "url": "https://mai.waifuism.life/files/1b0510f2-1fb4-43f5-a399-10053bbd8f0f" + }, + "id": "https://mai.waifuism.life/emojis/blobwtfnotlikethis", + "name": ":blobwtfnotlikethis:", + "type": "Emoji", + "updated": "2024-02-07T02:21:46.497Z" + } + ], + "type": "Like" +} +diff --git a/test/fixtures/mitra-custom-emoji-like.json b/test/fixtures/mitra-custom-emoji-like.json@@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://w3id.org/security/data-integrity/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#" + } + ], + "actor": "https://mitra.social/users/silverpill", + "cc": [], + "content": ":ablobcatheartsqueeze:", + "id": "https://mitra.social/activities/like/0195a89a-a3a0-ead4-3a1c-aa6311397cfd", + "object": "https://framapiaf.org/users/peertube/statuses/114182703352270287", + "proof": { + "created": "2025-03-18T09:34:21.610678375Z", + "cryptosuite": "eddsa-jcs-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5AvpwkXQGFpTneRVDNeF48Jo9qYG6PgrE5HaPPpQNdNyc31ULMN4Vxd4aFXELo4Rk5Y9hd9nDy254xP8v5uGGWp1", + "type": "DataIntegrityProof", + "verificationMethod": "https://mitra.social/users/silverpill#ed25519-key" + }, + "tag": [ + { + "attributedTo": "https://mitra.social/actor", + "icon": { + "mediaType": "image/png", + "type": "Image", + "url": "https://mitra.social/media/a08e153441b25e512ab1b2e8922f5d8cd928322c8b79958cd48297ac722a4117.png" + }, + "id": "https://mitra.social/objects/emojis/ablobcatheartsqueeze", + "name": ":ablobcatheartsqueeze:", + "type": "Emoji", + "updated": "1970-01-01T00:00:00Z" + } + ], + "to": [ + "https://framapiaf.org/users/peertube", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Like" +} +diff --git a/test/fixtures/rich_media/instagram_longtext.html b/test/fixtures/rich_media/instagram_longtext.html@@ -0,0 +1,90 @@ +<!DOCTYPE html><html class="_9dls _ar44" lang="en" dir="ltr"><head><link data-default-icon="https://static.cdninstagram.com/rsrc.php/v4/yI/r/VsNE-OHk_8a.png" rel="icon" sizes="192x192" href="https://static.cdninstagram.com/rsrc.php/v4/yI/r/VsNE-OHk_8a.png" /><meta name="bingbot" content="noarchive" /><meta name="robots" content="noarchive, noimageindex" /><meta charset="utf-8" /><meta name="apple-mobile-web-app-status-bar-style" content="default" /><meta name="mobile-web-app-capable" content="yes" /><meta id="viewport" name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=2, viewport-fit=cover" /><meta name="theme-color" content="#ffffff" /><meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" /><link rel="apple-touch-icon" sizes="76x76" href="https://static.cdninstagram.com/rsrc.php/v4/yR/r/lam-fZmwmvn.png" /><link rel="apple-touch-icon" sizes="120x120" href="https://static.cdninstagram.com/rsrc.php/v4/ys/r/aM-g435MtEX.png" /><link rel="apple-touch-icon" sizes="152x152" href="https://static.cdninstagram.com/rsrc.php/v4/yx/r/H1l_HHqi4p6.png" /><link rel="apple-touch-icon" sizes="167x167" href="https://static.cdninstagram.com/rsrc.php/v4/yB/r/-7Z_RkdLJUX.png" /><link rel="apple-touch-icon" sizes="180x180" href="https://static.cdninstagram.com/rsrc.php/v4/yG/r/De-Dwpd5CHc.png" /><link data-default-icon="https://static.cdninstagram.com/rsrc.php/y4/r/QaBlI0OZiks.ico" rel="shortcut icon" type="image/x-icon" href="https://static.cdninstagram.com/rsrc.php/y4/r/QaBlI0OZiks.ico" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/" hreflang="x-default" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=en" hreflang="en" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=fr" hreflang="fr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=it" hreflang="it" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=de" hreflang="de" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es" hreflang="es" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=zh-cn" hreflang="zh-cn" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=zh-tw" hreflang="zh-tw" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ja" hreflang="ja" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ko" hreflang="ko" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=pt" hreflang="pt" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=pt-br" hreflang="pt-br" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=af" hreflang="af" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=cs" hreflang="cs" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=da" hreflang="da" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=el" hreflang="el" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=fi" hreflang="fi" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=hr" hreflang="hr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=hu" hreflang="hu" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=id" hreflang="id" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ms" hreflang="ms" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=nb" hreflang="nb" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=nl" hreflang="nl" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=pl" hreflang="pl" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ru" hreflang="ru" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=sk" hreflang="sk" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=sv" hreflang="sv" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=th" hreflang="th" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=tl" hreflang="tl" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=tr" hreflang="tr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=hi" hreflang="hi" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=bn" hreflang="bn" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=gu" hreflang="gu" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=kn" hreflang="kn" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ml" hreflang="ml" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=mr" hreflang="mr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=pa" hreflang="pa" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ta" hreflang="ta" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=te" hreflang="te" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ne" hreflang="ne" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=si" hreflang="si" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=vi" hreflang="vi" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=bg" hreflang="bg" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=fr-ca" hreflang="fr-ca" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ro" hreflang="ro" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=sr" hreflang="sr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=uk" hreflang="uk" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=zh-hk" hreflang="zh-hk" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-ar" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-bo" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-cl" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-co" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-cr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-cu" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-do" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-ec" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-gt" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-hn" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-mx" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-ni" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-pa" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-pr" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-py" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-pe" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-sv" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-uy" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=es-la" hreflang="es-ve" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=he" hreflang="he" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=fa" hreflang="fa" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ar" hreflang="ar" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ur" hreflang="ur" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=en-gb" hreflang="en-gb" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=sw-ke" hreflang="sw-ke" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=ha-ng" hreflang="ha-ng" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=am-et" hreflang="am-et" /><link rel="alternate" href="https://www.instagram.com/p/DHTysJtvv4Q/?hl=om-et" hreflang="om-et" /><meta property="og:type" content="article" /><meta name="medium" content="image" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:site" content="@instagram" /><meta name="twitter:maxage" content="86400" /><meta name="twitter:image" content="https://scontent-sea1-1.cdninstagram.com/v/t51.75761-15/484875450_18496798228017668_844650360688897531_n.jpg?stp=c245.0.738.737a_dst-jpg_e35_s640x640_tt6&_nc_cat=108&ccb=1-7&_nc_sid=18de74&_nc_ohc=JEXvOhfYn6gQ7kNvgEDJBpe&_nc_zt=23&_nc_ht=scontent-sea1-1.cdninstagram.com&_nc_gid=Ih453OmD1GJ5weVP5m7vZQ&oh=00_AYFVFq3YMMFliBNSNVzrVBzRyMrvJPZlWEfRAnBwZiN5jg&oe=67E0C720" /><meta name="twitter:title" content="CAPTURE THE ATLAS (@capturetheatlas) • Instagram photos and videos" /><meta property="al:ios:app_name" content="Instagram" /><meta property="al:ios:app_store_id" content="389801252" /><meta property="al:ios:url" content="instagram://media?id=3590436264358116880" /><meta property="al:android:app_name" content="Instagram" /><meta property="al:android:package" content="com.instagram.android" /><meta property="al:android:url" content="https://www.instagram.com/p/DHTysJtvv4Q/" /><meta property="fb:app_id" content="124024574287414" /><meta property="instapp:owner_user_id" content="402833667" /><meta name="description" content="6,730 likes, 407 comments - capturetheatlas on March 17, 2025: "✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚 + +Last Thursday night, under the freezing skies of Northern Alaska, I witnessed something I never thought I’d see in my lifetime… + +A Total Lunar Eclipse—with the Moon fully immersed in Earth’s shadow—unfolded just as a powerful Aurora substorm exploded across the sky, painting the darkness with surreal waves of green and purple light. + +I traveled thousands of miles, braving extreme cold and unpredictable weather, hoping for this rare celestial alignment. These two events rarely coincide—but on this night, everything lined up perfectly. + +📸 Swipe to see the images & timelapse! Full-res shots are on my site at Capture the Atlas. You can also check out the RAW bracketed files from my close-up shot in my pinned IG stories. + +This is why I chase the night sky—because sometimes, the universe gives us pure magic. Have you ever seen an eclipse or the Northern Lights? Let me know in the comments! ⬇️ + +#LunarEclipse #AuroraBorealis #Astrophotography #TotalLunarEclipse #NorthernLights #CaptureTheAtlas #EclipseChasing #NightSky #SpaceWeather". " /><meta property="og:site_name" content="Instagram" /><meta property="og:title" content="CAPTURE THE ATLAS on Instagram: "✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚 + +Last Thursday night, under the freezing skies of Northern Alaska, I witnessed something I never thought I’d see in my lifetime… + +A Total Lunar Eclipse—with the Moon fully immersed in Earth’s shadow—unfolded just as a powerful Aurora substorm exploded across the sky, painting the darkness with surreal waves of green and purple light. + +I traveled thousands of miles, braving extreme cold and unpredictable weather, hoping for this rare celestial alignment. These two events rarely coincide—but on this night, everything lined up perfectly. + +📸 Swipe to see the images & timelapse! Full-res shots are on my site at Capture the Atlas. You can also check out the RAW bracketed files from my close-up shot in my pinned IG stories. + +This is why I chase the night sky—because sometimes, the universe gives us pure magic. Have you ever seen an eclipse or the Northern Lights? Let me know in the comments! ⬇️ + +#LunarEclipse #AuroraBorealis #Astrophotography #TotalLunarEclipse #NorthernLights #CaptureTheAtlas #EclipseChasing #NightSky #SpaceWeather"" /><meta property="og:image" content="https://scontent-sea1-1.cdninstagram.com/v/t51.75761-15/484875450_18496798228017668_844650360688897531_n.jpg?stp=c245.0.738.737a_dst-jpg_e35_s640x640_tt6&_nc_cat=108&ccb=1-7&_nc_sid=18de74&_nc_ohc=JEXvOhfYn6gQ7kNvgEDJBpe&_nc_zt=23&_nc_ht=scontent-sea1-1.cdninstagram.com&_nc_gid=Ih453OmD1GJ5weVP5m7vZQ&oh=00_AYFVFq3YMMFliBNSNVzrVBzRyMrvJPZlWEfRAnBwZiN5jg&oe=67E0C720" /><meta property="og:url" content="https://www.instagram.com/p/DHTysJtvv4Q/" /><meta property="og:description" content="6,730 likes, 407 comments - capturetheatlas on March 17, 2025: "✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚 + +Last Thursday night, under the freezing skies of Northern Alaska, I witnessed something I never thought I’d see in my lifetime… + +A Total Lunar Eclipse—with the Moon fully immersed in Earth’s shadow—unfolded just as a powerful Aurora substorm exploded across the sky, painting the darkness with surreal waves of green and purple light. + +I traveled thousands of miles, braving extreme cold and unpredictable weather, hoping for this rare celestial alignment. These two events rarely coincide—but on this night, everything lined up perfectly. + +📸 Swipe to see the images & timelapse! Full-res shots are on my site at Capture the Atlas. You can also check out the RAW bracketed files from my close-up shot in my pinned IG stories. + +This is why I chase the night sky—because sometimes, the universe gives us pure magic. Have you ever seen an eclipse or the Northern Lights? Let me know in the comments! ⬇️ + +#LunarEclipse #AuroraBorealis #Astrophotography #TotalLunarEclipse #NorthernLights #CaptureTheAtlas #EclipseChasing #NightSky #SpaceWeather". " /><link rel="canonical" href="https://www.instagram.com/p/DHTysJtvv4Q/" /><link rel="manifest" href="/data/manifest.json" crossorigin="use-credentials" /><meta name="color-scheme" content="light" /><style data-btmanifest="1021044255_main" nonce="itETU03f">:root, .__ig-light-mode:root, .__ig-light-mode {--fds-black:#000000;--fds-black-alpha-05:rgba(0, 0, 0, 0.05);--fds-black-alpha-10:rgba(0, 0, 0, 0.1);--fds-black-alpha-15:rgba(0, 0, 0, 0.15);--fds-black-alpha-20:rgba(0, 0, 0, 0.2);--fds-black-alpha-30:rgba(0, 0, 0, 0.3);--fds-black-alpha-40:rgba(0, 0, 0, 0.4);--fds-black-alpha-50:rgba(0, 0, 0, 0.5);--fds-black-alpha-60:rgba(0, 0, 0, 0.6);--fds-black-alpha-80:rgba(0, 0, 0, 0.8);--fds-blue-05:#ECF3FF;--fds-blue-30:#AAC9FF;--fds-blue-40:#77A7FF;--fds-blue-60:#1877F2;--fds-blue-70:#2851A3;--fds-blue-80:#1D3C78;--fds-button-text:#444950;--fds-comment-background:#F2F3F5;--fds-dark-mode-gray-35:#CCCCCC;--fds-dark-mode-gray-50:#828282;--fds-dark-mode-gray-70:#4A4A4A;--fds-dark-mode-gray-80:#373737;--fds-dark-mode-gray-90:#282828;--fds-dark-mode-gray-100:#1C1C1C;--fds-gray-00:#F5F6F7;--fds-gray-05:#F2F3F5;--fds-gray-10:#EBEDF0;--fds-gray-20:#DADDE1;--fds-gray-25:#CCD0D5;--fds-gray-30:#BEC3C9;--fds-gray-45:#8D949E;--fds-gray-70:#606770;--fds-gray-80:#444950;--fds-gray-90:#303338;--fds-gray-100:#1C1E21;--fds-green-55:#00A400;--fds-green-65:#51CE70;--fds-highlight:#3578E5;--fds-highlight-cell-background:#ECF3FF;--fds-primary-icon:#1C1E21;--fds-primary-text:#1C1E21;--fds-red-55:#FA383E;--fds-soft:cubic-bezier(.08,.52,.52,1);--fds-spectrum-aluminum-tint-70:#E4F0F6;--fds-spectrum-blue-gray-tint-70:#CFD1D5;--fds-spectrum-cherry:#F35369;--fds-spectrum-cherry-tint-70:#FBCCD2;--fds-spectrum-grape-tint-70:#DDD5F0;--fds-spectrum-grape-tint-90:#F4F1FA;--fds-spectrum-lemon-dark-1:#F5C33B;--fds-spectrum-lemon-tint-70:#FEF2D1;--fds-spectrum-lime:#A3CE71;--fds-spectrum-lime-tint-70:#E4F0D5;--fds-spectrum-orange-tint-70:#FCDEC5;--fds-spectrum-orange-tint-90:#FEF4EC;--fds-spectrum-seafoam-tint-70:#CAEEF9;--fds-spectrum-slate-dark-2:#89A1AC;--fds-spectrum-slate-tint-70:#EAEFF2;--fds-spectrum-teal:#6BCEBB;--fds-spectrum-teal-dark-1:#4DBBA6;--fds-spectrum-teal-dark-2:#31A38D;--fds-spectrum-teal-tint-70:#D2F0EA;--fds-spectrum-teal-tint-90:#F0FAF8;--fds-spectrum-tomato:#FB724B;--fds-spectrum-tomato-tint-30:#F38E7B;--fds-spectrum-tomato-tint-90:#FDEFED;--fds-strong:cubic-bezier(.12,.8,.32,1);--fds-unified-blue-35:#1455B0;--fds-unified-gray-20:#323436;--fds-white:#FFFFFF;--fds-white-alpha-05:rgba(255, 255, 255, 0.05);--fds-white-alpha-10:rgba(255, 255, 255, 0.1);--fds-white-alpha-20:rgba(255, 255, 255, 0.2);--fds-white-alpha-30:rgba(255, 255, 255, 0.3);--fds-white-alpha-40:rgba(255, 255, 255, 0.4);--fds-white-alpha-50:rgba(255, 255, 255, 0.5);--fds-white-alpha-60:rgba(255, 255, 255, 0.6);--fds-white-alpha-80:rgba(255, 255, 255, 0.8);--fds-yellow-20:#FFBA00;--accent:#0095F6;--always-white:#FFFFFF;--always-black:black;--always-dark-gradient:linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.6));--always-dark-overlay:rgba(0, 0, 0, 0.4);--always-light-overlay:rgba(255, 255, 255, 0.4);--always-gray-40:#65676B;--always-gray-75:#BCC0C4;--always-gray-95:#F0F2F5;--attachment-footer-background:#F0F2F5;--background-deemphasized:#F0F2F5;--badge-background-color-blue:var(--accent);--badge-background-color-dark-gray:var(--secondary-icon);--badge-background-color-gray:var(--disabled-icon);--badge-background-color-green:var(--positive);--badge-background-color-light-blue:var(--highlight-bg);--badge-background-color-red:var(--notification-badge);--badge-background-color-yellow:var(--base-lemon);--base-blue:#1877F2;--base-cherry:#F3425F;--base-grape:#9360F7;--base-lemon:#F7B928;--base-lime:#45BD62;--base-pink:#FF66BF;--base-seafoam:#54C7EC;--base-teal:#2ABBA7;--base-tomato:#FB724B;--text-badge-info-background:hsl(214, 89%, 52%);--text-badge-success-background:#31A24C;--text-badge-attention-background:hsl(40, 89%, 52%);--text-badge-critical-background:#e41e3f;--blue-link:#00376B;--border-focused:#65676B;--card-background:#FFFFFF;--card-background-flat:#F7F8FA;--comment-background:#F0F2F5;--comment-footer-background:#F6F9FA;--dataviz-primary-1:rgb(0,174,143);--disabled-button-background:rgba(0, 149, 246, 0.3);--disabled-button-text:#FFFFFF;--disabled-icon:#BCC0C4;--disabled-text:#BCC0C4;--divider:#DBDBDB;--event-date:#F3425F;--fb-wordmark:#0866FF;--fb-logo:#0866FF;--filter-accent:invert(39%) sepia(57%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(147.75%) hue-rotate(202deg) brightness(97%) contrast(96%);--filter-always-white:invert(100%);--filter-disabled-icon:invert(80%) sepia(6%) saturate(200%) saturate(120%) hue-rotate(173deg) brightness(98%) contrast(89%);--filter-placeholder-icon:invert(59%) sepia(11%) saturate(200%) saturate(135%) hue-rotate(176deg) brightness(96%) contrast(94%);--filter-primary-accent:invert(39%) sepia(57%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(147.75%) hue-rotate(202deg) brightness(97%) contrast(96%);--filter-primary-icon:invert(8%) sepia(10%) saturate(200%) saturate(200%) saturate(166%) hue-rotate(177deg) brightness(104%) contrast(91%);--filter-secondary-button-icon-on-media:invert(100%);--filter-secondary-icon:invert(39%) sepia(21%) saturate(200%) saturate(109.5%) hue-rotate(174deg) brightness(94%) contrast(86%);--filter-warning-icon:invert(77%) sepia(29%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(128%) hue-rotate(359deg) brightness(102%) contrast(107%);--filter-blue-link-icon:invert(30%) sepia(98%) saturate(200%) saturate(200%) saturate(200%) saturate(166.5%) hue-rotate(192deg) brightness(91%) contrast(101%);--filter-positive:invert(37%) sepia(61%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(115%) hue-rotate(91deg) brightness(97%) contrast(105%);--filter-primary-deemphasized-button-icon:invert(28%) sepia(100%) saturate(6042%) hue-rotate(202deg) brightness(96%) contrast(101%);--filter-negative:invert(25%) sepia(33%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(110%) hue-rotate(345deg) brightness(132%) contrast(96%);--focus-ring-blue:#1877F2;--glimmer-spinner-icon:#65676B;--hero-banner-background:#FFFFFF;--hosted-view-selected-state:rgba(45, 136, 255, 0.1);--highlight-bg:#E7F3FF;--hover-overlay:rgba(0, 0, 0, 0.05);--inverse-text:var(--always-white);--list-cell-chevron:#65676B;--media-hover:rgba(68, 73, 80, 0.15);--media-inner-border:rgba(0, 0, 0, 0.1);--media-outer-border:#FFFFFF;--media-pressed:rgba(68, 73, 80, 0.35);--messenger-card-background:#FFFFFF;--messenger-card-box-shadow:0px 1px 2px 0px #0000001A;--mwp-header-background-color:var(--messenger-card-background);--mwp-header-button-color:var(--accent);--mwp-message-row-background:var(--messenger-card-background);--messenger-reply-background:#F0F2F5;--overlay-alpha-80:rgba(0, 0, 0, 0.65);--overlay-on-media:rgba(0, 0, 0, 0.6);--nav-bar-background:#FFFFFF;--popover-card-background:var(--card-background);--nav-bar-background-gradient:linear-gradient(to top, #FFFFFF, rgba(255,255,255.9), rgba(255,255,255,.7), rgba(255,255,255,.4), rgba(255,255,255,0));--nav-bar-background-gradient-wash:linear-gradient(to top, #F0F2F5, rgba(240,242,245.9), rgba(240,242,245,.7), rgba(240,242,245,.4), rgba(240,242,245,0));--negative:hsl(350, 87%, 55%);--negative-background:hsl(350, 87%, 55%, 20%);--new-notification-background:#E7F3FF;--non-media-pressed:rgba(68, 73, 80, 0.15);--non-media-pressed-on-dark:rgba(255, 255, 255, 0.3);--notification-badge:#e41e3f;--placeholder-icon:#65676B;--placeholder-image:rgb(164, 167, 171);--placeholder-text:#65676B;--placeholder-text-on-media:rgba(255, 255, 255, 0.5);--popover-background:#FFFFFF;--positive:#31A24C;--positive-background:#DEEFE1;--press-overlay:rgba(0, 0, 0, 0.10);--primary-button-background:#0095F6;--primary-button-icon:#FFFFFF;--primary-button-pressed:#77A7FF;--primary-button-text:#FFFFFF;--primary-deemphasized-button-background:rgba(0, 149, 246, 0.1);--primary-deemphasized-button-pressed:rgba(0, 149, 246, 0.05);--primary-deemphasized-button-pressed-overlay:rgba(0, 149, 246, 0.15);--primary-deemphasized-button-text:#0095F6;--primary-icon:#262626;--primary-text:#262626;--primary-text-on-media:#FFFFFF;--primary-web-focus-indicator:#D24294;--progress-ring-neutral-background:rgba(0, 0, 0, 0.2);--progress-ring-neutral-foreground:#000000;--progress-ring-on-media-background:rgba(255, 255, 255, 0.2);--progress-ring-on-media-foreground:#FFFFFF;--progress-ring-blue-background:rgba(24, 119, 242, 0.2);--progress-ring-blue-foreground:hsl(214, 89%, 52%);--progress-ring-disabled-background:rgba(190,195,201, 0.2);--progress-ring-disabled-foreground:#BEC3C9;--rating-star-active:#EB660D;--scroll-thumb:#BCC0C4;--scroll-shadow:0 1px 2px rgba(0, 0, 0, 0.1), 0 -1px rgba(0, 0, 0, 0.1) inset;--secondary-button-background:transparent;--secondary-button-background-floating:#ffffff;--secondary-button-background-on-dark:rgba(0, 0, 0, 0.4);--secondary-button-pressed:rgba(0, 0, 0, 0.05);--secondary-button-stroke:transparent;--secondary-button-text:#0095F6;--secondary-icon:#8E8E8E;--secondary-text:#8E8E8E;--secondary-text-on-media:rgba(255, 255, 255, 0.9);--section-header-text:#4B4C4F;--shadow-1:rgba(0, 0, 0, 0.1);--shadow-2:rgba(0, 0, 0, 0.2);--shadow-5:rgba(0, 0, 0, 0.5);--shadow-8:rgba(0, 0, 0, 0.8);--shadow-base:0 1px 2px var(--shadow-2);--shadow-elevated:0 8px 20px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1);--shadow-emphasis:0 2px 12px var(--shadow-2);--shadow-inset:rgba(255, 255, 255, 0.5);--shadow-on-media:0px 0px 1px rgba(0, 0, 0, 0.62);--shadow-persistent:0px 0px 12px rgba(52, 72, 84, 0.05);--shadow-primary:0px 5px 12px rgba(52, 72, 84, 0.2);--shadow-responsive:0px 8px 16px 0px rgba(0, 0, 0, 0.1);--surface-background:#FFFFFF;--switch-active:hsl(214, 89%, 52%);--switch-inactive:#DBDBDB;--switch-unchecked-background-color:#8F9297;--text-highlight:rgba(24, 119, 242, 0.2);--input-background:#FFFFFF;--input-background-hover:var(--input-background);--input-background-warn-hover:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-hover:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-active:hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.05);--input-background-warn-active:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-active:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-disabled:#F0F2F5;--input-border-color:#CED0D4;--input-border-color-hover:var(--placeholder-text);--input-label-color-highlighted:hsl(214, 89%, 52%);--input-label-color-inside:var(--secondary-text);--input-label-color-error:var(--negative);--input-label-color-disabled:var(--disabled-text);--text-input-outside-label:#000000;--toast-background:#FFFFFF;--toast-text:#1C2B33;--toast-text-link:#216FDB;--toast-border-color:transparent;--toast-border-style:none;--toast-border-width:0px;--toggle-active-background:#E7F3FF;--toggle-active-icon:rgb(24, 119, 242);--toggle-active-text:rgb(24, 119, 242);--toggle-button-active-background:#E7F3FF;--tooltip-background:rgba(244, 244, 244, 0.8);--tooltip-box-shadow:0 2px 4px 0 var(--shadow-5);--popover-border-color:transparent;--wash:#FAFAFA;--web-wash:#FAFAFA;--warning:hsl(40, 89%, 52%);--chat-bubble-emphasis-ring-after:black;--chat-bubble-emphasis-ring-before:white;--chat-text-blockquote-color-background-line:rgba(0, 0, 0, 0.12);--chat-text-blockquote-color-text-primary-media:rgba(255, 255, 255, 0.7);--chat-incoming-message-bubble-background-color:#F0F0F0;--chat-outgoing-message-bubble-background-color:var(--accent);--chat-replied-message-background-color:rgba(0, 0, 0, 0.03);--radio-border-color:var(--primary-icon);--radio-border-color-disabled:var(--disabled-button-background);--radio-border-color-selected:var(--accent);--radio-checked-icon-color:var(--accent);--radio-checked-icon-color-disabled:var(--disabled-button-background);--dialog-anchor-vertical-padding:56px;--header-height:0px;--global-panel-width:0px;--global-panel-width-expanded:0px;--alert-banner-corner-radius:8px;--button-corner-radius:4px;--button-corner-radius-medium:10px;--button-corner-radius-large:12px;--button-height-large:40px;--button-height-medium:36px;--button-padding-horizontal-large:16px;--button-padding-horizontal-medium:16px;--button-padding-icon-only:16px;--button-icon-padding-large:16px;--button-icon-padding-medium:16px;--button-inner-icon-spacing-large:3px;--button-inner-icon-spacing-medium:3px;--blueprint-button-height-medium:40px;--blueprint-button-height-large:48px;--card-corner-radius:4px;--card-box-shadow:0 12px 28px 0 var(--shadow-2), 0 2px 4px 0 var(--shadow-1);--card-padding-horizontal:10px;--card-padding-vertical:20px;--chip-corner-radius:6px;--comment-bubble:18px;--dialog-corner-radius:8px;--glimmer-corner-radius:8px;--image-corner-radius:4px;--infochip-medium-radius:12px;--input-corner-radius:6px;--input-border-width:1px;--nav-list-cell-corner-radius:8px;--list-cell-corner-radius:8px;--list-cell-min-height:52px;--list-cell-padding-vertical:20px;--list-cell-padding-vertical-with-addon:14px;--menu-base-list-item-padding-horizontal:8px;--menu-base-list-item-padding-vertical:12px;--menu-base-size-full-margin-end:48px;--menu-item-base-margin-horizontal:8px;--menu-item-base-margin-vertical:0px;--menu-item-base-padding-horizontal:8px;--menu-item-base-overlay-radius:4px;--menu-item-base-padding-vertical:12px;--menu-item-base-with-icon-padding-horizontal:var(--menu-item-base-padding-horizontal);--separator-menu-item-margin-horizontal:16px;--separator-menu-item-margin-vertical:4px;--nav-list-cell-min-height:0px;--nav-list-cell-padding-vertical:16px;--nav-list-cell-padding-vertical-with-addon:16px;--nux-card-body-padding-end:24px;--page-footer-padding-vertical:16px;--popover-border-style:none;--popover-border-width:0px;--section-header-addOnEnd-margin-horizontal:8px;--section-header-addOnStart-margin-horizontal:12px;--section-header-addOnEnd-button-padding-horizontal:0px;--section-header-addOnEnd-button-padding-vertical:0px;--section-header-padding-vertical:16px;--section-header-subtitle-margin-vertical:14px;--section-header-subtitle-with-addOnEnd-margin-vertical:6px;--tab-height:60px;--tab-icon-padding-end:0px;--tab-text-icon-gap:4px;--tab-underline-color:transparent;--tab-underline-height:3px;--text-badge-corner-radius:4px;--text-badge-padding-horizontal:6px;--text-badge-padding-vertical:6px;--text-input-multi-padding-between-text-scrollbar:20px;--text-input-multi-padding-scrollbar:16px;--text-input-caption-margin-top:10px;--text-input-padding-vertical:12px;--toast-addon-padding-horizontal:6px;--toast-addon-padding-vertical:6px;--toast-container-max-width:100%;--toast-container-min-width:288px;--toast-container-padding-horizontal:10px;--toast-container-padding-vertical:16px;--toast-corner-radius:8px;--toaster-view-max-width:328px;--tooltip-corner-radius:8px;--typeahead-list-outer-padding-vertical:2px;--fds-animation-enter-exit-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-enter-exit-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-swap-shuffle-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-swap-shuffle-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-move-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-move-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-passive-move-in:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-passive-move-out:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-quick-move-in:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-quick-move-out:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-fade-in:cubic-bezier(0, 0, 1, 1);--fds-animation-fade-out:cubic-bezier(0, 0, 1, 1);--fds-duration-extra-extra-short-in:100ms;--fds-duration-extra-extra-short-out:100ms;--fds-duration-extra-short-in:200ms;--fds-duration-extra-short-out:150ms;--fds-duration-short-in:280ms;--fds-duration-short-out:200ms;--fds-duration-medium-in:400ms;--fds-duration-medium-out:350ms;--fds-duration-long-in:500ms;--fds-duration-long-out:350ms;--fds-duration-extra-long-in:1000ms;--fds-duration-extra-long-out:1000ms;--fds-duration-none:0ms;--fds-fast:200ms;--fds-slow:400ms;--font-family-apple:system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', sans-serif;--font-family-code:ui-monospace, Menlo, Consolas, Monaco, monospace;--font-family-default:Helvetica, Arial, sans-serif;--font-family-segoe:Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif;--body-font-family:Placeholder Font;--body-font-size:0.9375rem;--body-font-weight:400;--body-line-height:1.3333;--body-emphasized-font-family:Placeholder Font;--body-emphasized-font-size:0.9375rem;--body-emphasized-font-weight:600;--body-emphasized-line-height:1.3333;--headline1-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline1-font-size:1.75rem;--headline1-font-weight:700;--headline1-line-height:1.2143;--headline2-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline2-font-size:1.5rem;--headline2-font-weight:700;--headline2-line-height:1.25;--headline3-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline3-font-size:1.0625rem;--headline3-font-weight:700;--headline3-line-height:1.2941;--meta-font-family:Placeholder Font;--meta-font-size:0.8125rem;--meta-font-weight:400;--meta-line-height:1.3846;--meta-emphasized-font-family:Placeholder Font;--meta-emphasized-font-size:0.8125rem;--meta-emphasized-font-weight:600;--meta-emphasized-line-height:1.3846;--primary-label-font-family:Optimistic Display Medium, system-ui, sans-serif;--primary-label-font-size:1.0625rem;--primary-label-font-weight:500;--primary-label-line-height:1.2941;--secondary-label-font-family:Placeholder Font;--secondary-label-font-size:0.9375rem;--secondary-label-font-weight:500;--secondary-label-line-height:1.3333;--tertiary-label-font-family:Placeholder Font;--tertiary-label-font-size:0.6875rem;--tertiary-label-font-weight:500;--tertiary-label-line-height:1.4545;--text-input-field-font-family:Placeholder Font;--text-input-field-font-size:1rem;--text-input-field-font-stretch:initial;--text-input-field-font-weight:500;--text-input-field-line-height:1.2941;--text-input-label-font-family:Placeholder Font;--text-input-label-font-size:17px;--text-input-label-font-size-scale-multiplier:0.75;--text-input-label-font-stretch:initial;--text-input-label-font-weight:400;--text-input-label-line-height:1.2941;--messenger-card-min-width:300px;--messenger-card-max-width:480px;--messenger-application-max-width:2560px;--messenger-sidebar-collapsed-width:44px;--messenger-sidebar-expanded-width:240px;--messenger-card-corner-radius:8px;--messenger-card-spacing:16px;--chat-bubble-padding-vertical:8px;--chat-bubble-padding-horizontal:12px;--chat-bubble-border-radius:18px;--dialog-size-medium-width:100%;--dialog-size-small-width:100%;--focus-ring-outline-forced-colors:2px auto transparent !important;--focus-ring-outline-link:2px auto var(--focus-ring-blue);--focus-ring-shadow-default:0 0 0 2px var(--always-white), 0 0 0 4px var(--focus-ring-blue);--focus-ring-shadow-inset:0 0 0 2px var(--focus-ring-blue) inset, 0 0 0 4px var(--always-white) inset;--glimmer-animation-direction:alternate;--glimmer-animation-duration:1000ms;--glimmer-animation-timing-function:steps(10, end);--glimmer-opacity-min:0.25;--glimmer-opacity-max:1;--radio-checked-icon-size-large:12px;--radio-checked-icon-size-medium:12px;--radio-border-width:2px;--radio-size-large:24px;--radio-size-medium:20px;--blue-primary:rgb(0,136,244);--blue-secondary:rgb(4,59,114);--blue-tertiary:rgb(235,245,255);--chartreuse-primary:rgb(106,147,22);--chartreuse-secondary:rgb(50,65,19);--chartreuse-tertiary:rgb(226,255,152);--cyan-primary:rgb(0,142,213);--cyan-secondary:rgb(15,63,93);--cyan-tertiary:rgb(231,245,255);--dataviz-primary-2:rgb(156,219,255);--dataviz-primary-3:rgb(73,156,255);--dataviz-secondary-1:rgb(83,78,191);--dataviz-secondary-2:rgb(103,96,228);--dataviz-secondary-3:rgb(119,45,88);--dataviz-supplementary-1:rgb(253,91,67);--dataviz-supplementary-2:rgb(161,132,0);--dataviz-supplementary-3:rgb(36,131,44);--dataviz-supplementary-4:rgb(51,51,52);--fuschia-primary:rgb(250,45,138);--fuschia-secondary:rgb(118,14,62);--fuschia-tertiary:rgb(255,241,246);--green-primary:rgb(43,154,53);--green-secondary:rgb(26,68,27);--green-tertiary:rgb(227,250,224);--magenta-primary:rgb(215,77,204);--magenta-secondary:rgb(96,36,91);--magenta-tertiary:rgb(255,239,254);--orange-primary:rgb(208,108,20);--orange-secondary:rgb(94,49,14);--orange-tertiary:rgb(255,241,239);--purple-primary:rgb(125,116,255);--purple-secondary:rgb(55,50,121);--purple-tertiary:rgb(245,241,255);--red-primary:rgb(251,60,68);--red-secondary:rgb(118,22,27);--red-tertiary:rgb(255,241,239);--teal-primary:rgb(0,152,124);--teal-secondary:rgb(10,68,56);--teal-tertiary:rgb(227,247,241);--yellow-primary:rgb(161,132,0);--yellow-secondary:rgb(72,59,12);--yellow-tertiary:rgb(255,246,161);--base-unit:4px;--blue-0:245, 251, 255;--blue-1:224, 241, 255;--blue-2:179, 219, 255;--blue-3:112, 188, 255;--blue-4:71, 175, 255;--blue-5:0, 149, 246;--blue-6:0, 116, 204;--blue-7:0, 87, 163;--blue-8:0, 55, 107;--blue-9:0, 41, 82;--breakpoint-medium-width:1536px;--breakpoint-small-width:1024px;--challenge-width:460px;--creation-header-height:43px;--creation-min-padding-x:32px;--creation-modal-max-height:898px;--creation-modal-min-height:391px;--creation-padding-x:64px;--creation-padding-y:112px;--creation-settings-width:340px;--cyan-5:39, 196, 245;--desktop-grid-item-margin:28px;--desktop-grid-item-margin-slim:4px;--desktop-in-feed-story-item-height:208px;--desktop-in-feed-story-item-width:116px;--desktop-nav-height:60px;--desktop-skinny-nav-height:60px;--desktop-story-tray-bottom-margin:48px;--desktop-story-tray-bottom-margin-denser:24px;--direct-attachment-image-grid-item-size:78px;--direct-attachment-story-height:150px;--direct-attachment-story-large-height:256px;--direct-attachment-story-large-width:164px;--direct-attachment-story-width:84px;--direct-message-max-width:236px;--fb-signup-page-profile-pic-size:88px;--feed-sidebar-padding:32px;--feed-sidebar-width:319px;--feed-width:470px;--feed-width-wide-story:630px;--feed-width-wide-story-prism:702px;--font-family-system:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-family-system-prism:"Instagram Sans UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-weight-system-bold:700;--font-weight-system-extra-bold:800;--font-weight-system-extra-light:200;--font-weight-system-light:300;--font-weight-system-medium:500;--font-weight-system-regular:400;--font-weight-system-semibold:600;--footer-width-wide:1150px;--gradient-lavender:211, 0, 197;--gradient-orange:255, 122, 0;--gradient-pink:255, 1, 105;--gradient-purple:118, 56, 250;--gradient-red:255, 48, 64;--gradient-yellow:255, 214, 0;--green-4:120, 222, 69;--green-5:88, 195, 34;--green-6:55, 166, 0;--grey-0:245, 245, 245;--grey-1:239, 239, 239;--grey-2:219, 219, 219;--grey-3:199, 199, 199;--grey-4:168, 168, 168;--grey-5:142, 142, 142;--grey-6:115, 115, 115;--grey-7:85, 85, 85;--grey-8:54, 54, 54;--grey-9:38, 38, 38;--grey-10:26, 26, 26;--grey-0-prism:248, 249, 249;--grey-1-prism:243, 245, 247;--grey-2-prism:233, 237, 240;--grey-3-prism:219, 223, 228;--grey-4-prism:162, 170, 180;--grey-5-prism:131, 137, 147;--grey-6-prism:111, 118, 128;--grey-7-prism:94, 100, 109;--grey-8-prism:50, 53, 57;--grey-9-prism:41, 41, 46;--grey-10-prism:33, 35, 38;--ig-badge:255, 48, 64;--ig-close-friends-refreshed:28, 209, 79;--ig-disabled-action-text:169, 219, 255;--ig-error-or-destructive:237, 73, 86;--ig-facebook-blue:53, 121, 234;--ig-live-badge:255, 1, 105;--ig-primary-button:0, 149, 246;--ig-primary-button-hover:24, 119, 242;--ig-secondary-icon:142, 142, 142;--ig-stroke-on-media:255, 255, 255;--ig-tertiary-button-background:255, 255, 255;--ig-tertiary-button-border:219, 219, 219;--ig-tertiary-button-hover:245, 245, 245;--ig-tertiary-button-text:38, 38, 38;--ig-subscribers-only:118, 56, 250;--ig-success:88, 195, 34;--ig-text-on-color:255, 255, 255;--ig-text-on-media:255, 255, 255;--in-feed-story-item-height:240px;--in-feed-story-item-width:135px;--in-feed-story-item-padding:12px;--input-border-radius:6px;--large-layout-min:1500px;--live-video-border-radius:4px;--media-content-card-width:350px;--media-content-card-width-small:300px;--media-info:335px;--medium-layout-max:1499px;--medium-layout-min:1080px;--medium-screen-max:875px;--medium-screen-min:736px;--mobile-grid-item-margin:2px;--mobile-nav-height:45px;--modal-backdrop-dark:rgba(0, 0, 0, 0.85);--modal-backdrop-default:rgba(0, 0, 0, 0.65);--modal-border-radius:12px;--modal-padding:16px;--modal-z-index:100;--nav-narrow-width:72px;--nav-medium-width:244px;--nav-wide-width:335px;--nav-bottom-screen-max:767px;--nav-narrow-screen-min:768px;--nav-medium-screen-min:1264px;--nav-wide-screen-min:1920px;--orange-5:253, 141, 50;--photo:600px;--pink-5:209, 8, 105;--polaris-feed-with-padding-threshold-min:640px;--polaris-extra-small-screen-max:413px;--polaris-small-screen-min:414px;--polaris-small-screen-max:735px;--polaris-medium-screen-min:736px;--polaris-medium-screen-max:875px;--polaris-large-screen-min:876px;--polaris-nav-bottom-screen-max:767px;--polaris-post-modal-small-screen-max:1366px;--polaris-post-modal-large-screen-min:1536px;--polaris-post-modal-large-height-max:1032px;--polaris-challenge-width:460px;--polaris-breakpoint-medium-width:1536px;--polaris-breakpoint-small-width:1024px;--polaris-site-width-narrow:600px;--polaris-site-width-wide:935px;--polaris-site-padding-top:30px;--purple-5:163, 7, 186;--red-4:255, 104, 116;--red-5:237, 73, 86;--red-6:198, 35, 48;--red-7:167, 3, 17;--reels-large-screen-min:1366px;--refinement-section-height:50px;--revamp-nav-bottom-toolbar-height:50px;--revamp-feed-card-max-height:835px;--revamp-feed-card-min-height:615px;--revamp-feed-card-media-min-width:390px;--revamp-feed-card-dense-padding:16px;--revamp-feed-item-spacing:20px;--revamp-feed-horizontal-padding-small-screen:24px;--revamp-feed-horizontal-padding-large-screen:32px;--revamp-feed-vertical-padding:32px;--right-rail-width:300px;--scrollable-content-header-height-large:56px;--scrollable-content-header-height-med:49px;--scrollable-content-header-height:44px;--search-box-height:40px;--search-modal-height-expanded:450px;--search-modal-height:362px;--search-modal-top-offset:12px;--search-result-height:50px;--search-result-inline-top-offset:60px;--search-result-list-width:375px;--site-width-narrow:600px;--site-width-wide:935px;--small-layout-max:1079px;--small-layout-min:800px;--small-screen-max:735px;--small-screen-min:414px;--story-progressbar-update-tick:0.1s;--story-swap-animation-duration:350ms;--system-10-font-size:10px;--system-10-line-height:12px;--system-11-font-size:11px;--system-11-line-height:13px;--system-12-font-size:12px;--system-12-line-height:16px;--system-14-font-size:14px;--system-14-line-height:18px;--system-16-font-size:16px;--system-16-line-height:20px;--system-18-font-size:18px;--system-18-line-height:24px;--system-20-font-size:20px;--system-20-line-height:25px;--system-22-font-size:22px;--system-22-line-height:26px;--system-24-font-size:24px;--system-24-line-height:27px;--system-26-font-size:26px;--system-26-line-height:28px;--system-28-font-size:28px;--system-28-line-height:32px;--system-30-font-size:30px;--system-30-line-height:36px;--system-32-font-size:32px;--system-32-line-height:40px;--web-always-black:0, 0, 0;--web-always-white:255, 255, 255;--web-overlay-on-media:38, 38, 38;--web-prism-black:15, 20, 25;--web-secondary-action:224, 241, 255;--yellow-5:253, 203, 92;--barcelona-logo:0, 0, 0;--challenge-link:54, 54, 54;--ig-banner-background:255, 255, 255;--ig-elevated-background:255, 255, 255;--ig-elevated-highlight-background:239, 239, 239;--ig-elevated-separator:219, 219, 219;--ig-focus-stroke:168, 168, 168;--ig-highlight-background:239, 239, 239;--ig-hover-overlay:0, 0, 0, 0.05;--ig-link:0, 55, 107;--ig-primary-background:255, 255, 255;--ig-primary-icon:38, 38, 38;--ig-primary-text:0, 0, 0;--ig-secondary-background:245, 245, 245;--ig-secondary-button:38, 38, 38;--ig-secondary-button-background:239, 239, 239;--ig-secondary-button-hover:219, 219, 219;--ig-secondary-text:115, 115, 115;--ig-separator:219, 219, 219;--ig-stroke:219, 219, 219;--ig-temporary-highlight:245, 251, 255;--ig-tertiary-icon:199, 199, 199;--ig-tertiary-text:199, 199, 199;--post-separator:239, 239, 239;--tos-box-shadow:0, 0, 0;--ig-toggle-background-on-prism:15, 20, 25;--ig-toggle-background-off-prism:219, 223, 228;--ig-toggle-background-disabled-on-prism:162, 170, 180;--ig-toggle-background-disabled-off-prism:233, 237, 240;--ig-toggle-outline-prism:219, 223, 228;--ig-toggle-outline-disabled-prism:243, 245, 247;--ig-stroke-prism:255, 255, 255;--ig-stroke-disabled-on-prism:233, 237, 240;--ig-stroke-disabled-off-prism:248, 249, 249;--ig-dropdown-background:243, 245, 247;}@media (prefers-color-scheme: dark){:root {--fds-black:black;--fds-black-alpha-05:rgba(0, 0, 0, 0.05);--fds-black-alpha-10:rgba(0, 0, 0, 0.1);--fds-black-alpha-15:rgba(0, 0, 0, 0.15);--fds-black-alpha-20:rgba(0, 0, 0, 0.2);--fds-black-alpha-30:rgba(0, 0, 0, 0.3);--fds-black-alpha-40:rgba(0, 0, 0, 0.4);--fds-black-alpha-50:rgba(0, 0, 0, 0.5);--fds-black-alpha-60:rgba(0, 0, 0, 0.6);--fds-black-alpha-80:rgba(0, 0, 0, 0.8);--fds-blue-05:black;--fds-blue-30:black;--fds-blue-40:black;--fds-blue-60:black;--fds-blue-70:black;--fds-blue-80:black;--fds-button-text:black;--fds-comment-background:black;--fds-dark-mode-gray-35:black;--fds-dark-mode-gray-50:black;--fds-dark-mode-gray-70:black;--fds-dark-mode-gray-80:black;--fds-dark-mode-gray-90:black;--fds-dark-mode-gray-100:black;--fds-gray-00:black;--fds-gray-05:black;--fds-gray-10:black;--fds-gray-20:black;--fds-gray-25:black;--fds-gray-30:black;--fds-gray-45:black;--fds-gray-70:black;--fds-gray-80:black;--fds-gray-90:black;--fds-gray-100:black;--fds-green-55:black;--fds-green-65:black;--fds-highlight:black;--fds-highlight-cell-background:black;--fds-primary-icon:white;--fds-primary-text:white;--fds-red-55:black;--fds-soft:cubic-bezier(.08,.52,.52,1);--fds-spectrum-aluminum-tint-70:black;--fds-spectrum-blue-gray-tint-70:black;--fds-spectrum-cherry:black;--fds-spectrum-cherry-tint-70:black;--fds-spectrum-grape-tint-70:black;--fds-spectrum-grape-tint-90:black;--fds-spectrum-lemon-dark-1:black;--fds-spectrum-lemon-tint-70:black;--fds-spectrum-lime:black;--fds-spectrum-lime-tint-70:black;--fds-spectrum-orange-tint-70:black;--fds-spectrum-orange-tint-90:black;--fds-spectrum-seafoam-tint-70:black;--fds-spectrum-slate-dark-2:black;--fds-spectrum-slate-tint-70:black;--fds-spectrum-teal:black;--fds-spectrum-teal-dark-1:black;--fds-spectrum-teal-dark-2:black;--fds-spectrum-teal-tint-70:black;--fds-spectrum-teal-tint-90:black;--fds-spectrum-tomato:black;--fds-spectrum-tomato-tint-30:black;--fds-spectrum-tomato-tint-90:black;--fds-strong:cubic-bezier(.12,.8,.32,1);--fds-unified-blue-35:black;--fds-unified-gray-20:black;--fds-white:black;--fds-white-alpha-05:rgba(255, 255, 255, 0.05);--fds-white-alpha-10:rgba(255, 255, 255, 0.1);--fds-white-alpha-20:rgba(255, 255, 255, 0.2);--fds-white-alpha-30:rgba(255, 255, 255, 0.3);--fds-white-alpha-40:rgba(255, 255, 255, 0.4);--fds-white-alpha-50:rgba(255, 255, 255, 0.5);--fds-white-alpha-60:rgba(255, 255, 255, 0.6);--fds-white-alpha-80:rgba(255, 255, 255, 0.8);--fds-yellow-20:black;--accent:#0095F6;--always-white:white;--always-black:black;--always-dark-gradient:linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.6));--always-dark-overlay:rgba(0, 0, 0, 0.4);--always-light-overlay:rgba(255, 255, 255, 0.4);--always-gray-40:#65676B;--always-gray-75:#BCC0C4;--always-gray-95:#F0F2F5;--attachment-footer-background:rgba(255,255,255,0.1);--background-deemphasized:rgba(255,255,255,0.1);--badge-background-color-blue:var(--accent);--badge-background-color-dark-gray:var(--secondary-icon);--badge-background-color-gray:var(--disabled-icon);--badge-background-color-green:var(--positive);--badge-background-color-light-blue:var(--highlight-bg);--badge-background-color-red:var(--notification-badge);--badge-background-color-yellow:var(--base-lemon);--base-blue:#1877F2;--base-cherry:#F3425F;--base-grape:#9360F7;--base-lemon:#F7B928;--base-lime:#45BD62;--base-pink:#FF66BF;--base-seafoam:#54C7EC;--base-teal:#2ABBA7;--base-tomato:#FB724B;--text-badge-info-background:hsl(214, 100%, 59%);--text-badge-success-background:#31A24C;--text-badge-attention-background:hsl(40, 89%, 52%);--text-badge-critical-background:#e41e3f;--blue-link:#00376B;--border-focused:#8A8D91;--card-background:#242526;--card-background-flat:#323436;--comment-background:#3A3B3C;--comment-footer-background:#4E4F50;--dataviz-primary-1:rgb(0,174,143);--disabled-button-background:rgba(255, 255, 255, 0.2);--disabled-button-text:rgba(255, 255, 255, 0.3);--disabled-icon:rgba(255, 255, 255, 0.3);--disabled-text:rgba(255, 255, 255, 0.3);--divider:#3E4042;--event-date:#F3425F;--fb-wordmark:#FFFFFF;--fb-logo:#0866FF;--filter-accent:invert(40%) sepia(52%) saturate(200%) saturate(200%) saturate(200%) saturate(189%) hue-rotate(191deg) brightness(103%) contrast(102%);--filter-always-white:invert(100%);--filter-disabled-icon:invert(100%) opacity(30%);--filter-placeholder-icon:invert(59%) sepia(11%) saturate(200%) saturate(135%) hue-rotate(176deg) brightness(96%) contrast(94%);--filter-primary-accent:invert(40%) sepia(52%) saturate(200%) saturate(200%) saturate(200%) saturate(189%) hue-rotate(191deg) brightness(103%) contrast(102%);--filter-primary-icon:invert(89%) sepia(6%) hue-rotate(185deg);--filter-secondary-button-icon-on-media:invert(100%);--filter-secondary-icon:invert(62%) sepia(98%) saturate(12%) hue-rotate(175deg) brightness(90%) contrast(96%);--filter-warning-icon:invert(77%) sepia(29%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(128%) hue-rotate(359deg) brightness(102%) contrast(107%);--filter-blue-link-icon:invert(73%) sepia(29%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(103.25%) hue-rotate(189deg) brightness(101%) contrast(101%);--filter-positive:invert(37%) sepia(61%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(115%) hue-rotate(91deg) brightness(97%) contrast(105%);--filter-primary-deemphasized-button-icon:brightness(0) saturate(100%) invert(63%) sepia(20%) saturate(1290%) hue-rotate(185deg) brightness(107%) contrast(101%);--filter-negative:invert(25%) sepia(33%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(110%) hue-rotate(345deg) brightness(132%) contrast(96%);--focus-ring-blue:#1877F2;--glimmer-spinner-icon:white;--hero-banner-background:#E85D07;--hosted-view-selected-state:rgba(45, 136, 255, 0.1);--highlight-bg:rgba(24, 119, 242, .31);--hover-overlay:rgba(255, 255, 255, 0.1);--inverse-text:var(--always-white);--list-cell-chevron:#B0B3B8;--media-hover:rgba(68, 73, 80, 0.15);--media-inner-border:rgba(255, 255, 255, 0.05);--media-outer-border:#33363A;--media-pressed:rgba(68, 73, 80, 0.35);--messenger-card-background:#242526;--messenger-card-box-shadow:0px 0px 16px rgba(0, 0, 0, 0.3);--mwp-header-background-color:var(--messenger-card-background);--mwp-header-button-color:var(--accent);--mwp-message-row-background:var(--messenger-card-background);--messenger-reply-background:#18191A;--overlay-alpha-80:rgba(0, 0, 0, 0.65);--overlay-on-media:rgba(0, 0, 0, 0.6);--nav-bar-background:#242526;--popover-card-background:var(--card-background);--nav-bar-background-gradient:linear-gradient(to top, #242526, rgba(36,37,38,.9), rgba(36,37,38,.7), rgba(36,37,38,.4), rgba(36,37,38,0));--nav-bar-background-gradient-wash:linear-gradient(to top, #18191A, rgba(24,25,26,.9), rgba(24,25,26,.7), rgba(24,25,26,.4), rgba(24,25,26,0));--negative:hsl(350, 87%, 55%);--negative-background:hsl(350, 87%, 55%, 20%);--new-notification-background:#E7F3FF;--non-media-pressed:rgba(68, 73, 80, 0.15);--non-media-pressed-on-dark:rgba(255, 255, 255, 0.3);--notification-badge:#e41e3f;--placeholder-icon:#8A8D91;--placeholder-image:rgb(164, 167, 171);--placeholder-text:#8A8D91;--placeholder-text-on-media:rgba(255, 255, 255, 0.5);--popover-background:#3E4042;--positive:#31A24C;--positive-background:#1F3520;--press-overlay:rgba(255, 255, 255, 0.2);--primary-button-background:#0095F6;--primary-button-icon:#FFFFFF;--primary-button-pressed:#77A7FF;--primary-button-text:#FFFFFF;--primary-deemphasized-button-background:rgba(45, 136, 255, 0.2);--primary-deemphasized-button-pressed:rgba(24, 119, 242, 0.2);--primary-deemphasized-button-pressed-overlay:rgba(25, 110, 255, 0.15);--primary-deemphasized-button-text:#2D88FF;--primary-icon:#E4E6EB;--primary-text:#E4E6EB;--primary-text-on-media:white;--primary-web-focus-indicator:#D24294;--progress-ring-neutral-background:rgba(255, 255, 255, 0.2);--progress-ring-neutral-foreground:#ffffff;--progress-ring-on-media-background:rgba(255, 255, 255, 0.2);--progress-ring-on-media-foreground:#FFFFFF;--progress-ring-blue-background:rgba(45, 136, 255, 0.2);--progress-ring-blue-foreground:hsl(214, 100%, 59%);--progress-ring-disabled-background:rgba(122,125,130, 0.2);--progress-ring-disabled-foreground:#7A7D82;--rating-star-active:#FF9831;--scroll-thumb:rgba(255, 255, 255, 0.3);--scroll-shadow:0 1px 2px rgba(0, 0, 0, 0.1), 0 -1px rgba(255, 255, 255, 0.05) inset;--secondary-button-background:rgba(255,255,255,.1);--secondary-button-background-floating:#4B4C4F;--secondary-button-background-on-dark:rgba(255, 255, 255, 0.4);--secondary-button-pressed:rgba(0, 0, 0, 0.05);--secondary-button-stroke:transparent;--secondary-button-text:#E4E6EB;--secondary-icon:#B0B3B8;--secondary-text:#B0B3B8;--secondary-text-on-media:rgba(255, 255, 255, 0.9);--section-header-text:#BCC0C4;--shadow-1:rgba(0, 0, 0, 0.1);--shadow-2:rgba(0, 0, 0, 0.2);--shadow-5:rgba(0, 0, 0, 0.5);--shadow-8:rgba(0, 0, 0, 0.8);--shadow-base:0 1px 2px var(--shadow-2);--shadow-elevated:0 8px 20px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1);--shadow-emphasis:0 2px 12px var(--shadow-2);--shadow-inset:rgba(255, 255, 255, 0.05);--shadow-on-media:0px 0px 1px rgba(0, 0, 0, 0.62);--shadow-persistent:0px 0px 12px rgba(28, 43, 51, 0.6);--shadow-primary:0px 0px 12px rgba(28, 43, 51, 0.1);--shadow-responsive:0px 8px 16px 0px rgba(0, 0, 0, 0.1);--surface-background:#242526;--switch-active:hsl(214, 100%, 59%);--switch-inactive:#3E4042;--switch-unchecked-background-color:#6F7276;--text-highlight:rgba(24, 119, 242, 0.45);--input-background:#242526;--input-background-hover:var(--input-background);--input-background-warn-hover:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-hover:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-active:hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.05);--input-background-warn-active:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-active:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-disabled:#18191A;--input-border-color:#3E4042;--input-border-color-hover:var(--placeholder-text);--input-label-color-highlighted:hsl(214, 100%, 59%);--input-label-color-inside:var(--secondary-text);--input-label-color-error:var(--negative);--input-label-color-disabled:var(--disabled-text);--text-input-outside-label:#FFFFFF;--toast-background:#242526;--toast-text:#FFFFFF;--toast-text-link:#4599FF;--toast-border-color:transparent;--toast-border-style:none;--toast-border-width:0px;--toggle-active-background:rgb(45, 136, 255);--toggle-active-icon:#FFFFFF;--toggle-active-text:#FFFFFF;--toggle-button-active-background:#E6F2FF;--tooltip-background:rgba(11, 11, 11, 0.8);--tooltip-box-shadow:0 2px 4px 0 var(--shadow-5);--popover-border-color:transparent;--wash:#3E4042;--web-wash:#18191A;--warning:hsl(40, 89%, 52%);--chat-bubble-emphasis-ring-after:white;--chat-bubble-emphasis-ring-before:black;--chat-text-blockquote-color-background-line:rgba(255, 255, 255, 0.19);--chat-text-blockquote-color-text-primary-media:rgba(255, 255, 255, 0.7);--chat-incoming-message-bubble-background-color:#303030;--chat-outgoing-message-bubble-background-color:var(--accent);--chat-replied-message-background-color:rgba(255, 255, 255, 0.12);--radio-border-color:var(--primary-icon);--radio-border-color-disabled:var(--disabled-button-background);--radio-border-color-selected:var(--accent);--radio-checked-icon-color:var(--accent);--radio-checked-icon-color-disabled:var(--disabled-button-background);--dialog-anchor-vertical-padding:56px;--header-height:0px;--global-panel-width:0px;--global-panel-width-expanded:0px;--alert-banner-corner-radius:8px;--button-corner-radius:4px;--button-corner-radius-medium:10px;--button-corner-radius-large:12px;--button-height-large:40px;--button-height-medium:36px;--button-padding-horizontal-large:16px;--button-padding-horizontal-medium:16px;--button-padding-icon-only:16px;--button-icon-padding-large:16px;--button-icon-padding-medium:16px;--button-inner-icon-spacing-large:3px;--button-inner-icon-spacing-medium:3px;--blueprint-button-height-medium:40px;--blueprint-button-height-large:48px;--card-corner-radius:4px;--card-box-shadow:0 12px 28px 0 var(--shadow-2), 0 2px 4px 0 var(--shadow-1);--card-padding-horizontal:10px;--card-padding-vertical:20px;--chip-corner-radius:6px;--comment-bubble:18px;--dialog-corner-radius:8px;--glimmer-corner-radius:8px;--image-corner-radius:4px;--infochip-medium-radius:12px;--input-corner-radius:6px;--input-border-width:1px;--nav-list-cell-corner-radius:8px;--list-cell-corner-radius:8px;--list-cell-min-height:52px;--list-cell-padding-vertical:20px;--list-cell-padding-vertical-with-addon:14px;--menu-base-list-item-padding-horizontal:8px;--menu-base-list-item-padding-vertical:12px;--menu-base-size-full-margin-end:48px;--menu-item-base-margin-horizontal:8px;--menu-item-base-margin-vertical:0px;--menu-item-base-padding-horizontal:8px;--menu-item-base-overlay-radius:4px;--menu-item-base-padding-vertical:12px;--menu-item-base-with-icon-padding-horizontal:var(--menu-item-base-padding-horizontal);--separator-menu-item-margin-horizontal:16px;--separator-menu-item-margin-vertical:4px;--nav-list-cell-min-height:0px;--nav-list-cell-padding-vertical:16px;--nav-list-cell-padding-vertical-with-addon:16px;--nux-card-body-padding-end:24px;--page-footer-padding-vertical:16px;--popover-border-style:none;--popover-border-width:0px;--section-header-addOnEnd-margin-horizontal:8px;--section-header-addOnStart-margin-horizontal:12px;--section-header-addOnEnd-button-padding-horizontal:0px;--section-header-addOnEnd-button-padding-vertical:0px;--section-header-padding-vertical:16px;--section-header-subtitle-margin-vertical:14px;--section-header-subtitle-with-addOnEnd-margin-vertical:6px;--tab-height:60px;--tab-icon-padding-end:0px;--tab-text-icon-gap:4px;--tab-underline-color:transparent;--tab-underline-height:3px;--text-badge-corner-radius:4px;--text-badge-padding-horizontal:6px;--text-badge-padding-vertical:6px;--text-input-multi-padding-between-text-scrollbar:20px;--text-input-multi-padding-scrollbar:16px;--text-input-caption-margin-top:10px;--text-input-padding-vertical:12px;--toast-addon-padding-horizontal:6px;--toast-addon-padding-vertical:6px;--toast-container-max-width:100%;--toast-container-min-width:288px;--toast-container-padding-horizontal:10px;--toast-container-padding-vertical:16px;--toast-corner-radius:8px;--toaster-view-max-width:328px;--tooltip-corner-radius:8px;--typeahead-list-outer-padding-vertical:2px;--fds-animation-enter-exit-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-enter-exit-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-swap-shuffle-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-swap-shuffle-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-move-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-move-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-passive-move-in:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-passive-move-out:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-quick-move-in:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-quick-move-out:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-fade-in:cubic-bezier(0, 0, 1, 1);--fds-animation-fade-out:cubic-bezier(0, 0, 1, 1);--fds-duration-extra-extra-short-in:100ms;--fds-duration-extra-extra-short-out:100ms;--fds-duration-extra-short-in:200ms;--fds-duration-extra-short-out:150ms;--fds-duration-short-in:280ms;--fds-duration-short-out:200ms;--fds-duration-medium-in:400ms;--fds-duration-medium-out:350ms;--fds-duration-long-in:500ms;--fds-duration-long-out:350ms;--fds-duration-extra-long-in:1000ms;--fds-duration-extra-long-out:1000ms;--fds-duration-none:0ms;--fds-fast:200ms;--fds-slow:400ms;--font-family-apple:system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', sans-serif;--font-family-code:ui-monospace, Menlo, Consolas, Monaco, monospace;--font-family-default:Helvetica, Arial, sans-serif;--font-family-segoe:Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif;--body-font-family:Placeholder Font;--body-font-size:0.9375rem;--body-font-weight:400;--body-line-height:1.3333;--body-emphasized-font-family:Placeholder Font;--body-emphasized-font-size:0.9375rem;--body-emphasized-font-weight:600;--body-emphasized-line-height:1.3333;--headline1-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline1-font-size:1.75rem;--headline1-font-weight:700;--headline1-line-height:1.2143;--headline2-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline2-font-size:1.5rem;--headline2-font-weight:700;--headline2-line-height:1.25;--headline3-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline3-font-size:1.0625rem;--headline3-font-weight:700;--headline3-line-height:1.2941;--meta-font-family:Placeholder Font;--meta-font-size:0.8125rem;--meta-font-weight:400;--meta-line-height:1.3846;--meta-emphasized-font-family:Placeholder Font;--meta-emphasized-font-size:0.8125rem;--meta-emphasized-font-weight:600;--meta-emphasized-line-height:1.3846;--primary-label-font-family:Optimistic Display Medium, system-ui, sans-serif;--primary-label-font-size:1.0625rem;--primary-label-font-weight:500;--primary-label-line-height:1.2941;--secondary-label-font-family:Placeholder Font;--secondary-label-font-size:0.9375rem;--secondary-label-font-weight:500;--secondary-label-line-height:1.3333;--tertiary-label-font-family:Placeholder Font;--tertiary-label-font-size:0.6875rem;--tertiary-label-font-weight:500;--tertiary-label-line-height:1.4545;--text-input-field-font-family:Placeholder Font;--text-input-field-font-size:1rem;--text-input-field-font-stretch:initial;--text-input-field-font-weight:500;--text-input-field-line-height:1.2941;--text-input-label-font-family:Placeholder Font;--text-input-label-font-size:17px;--text-input-label-font-size-scale-multiplier:0.75;--text-input-label-font-stretch:initial;--text-input-label-font-weight:400;--text-input-label-line-height:1.2941;--messenger-card-min-width:300px;--messenger-card-max-width:480px;--messenger-application-max-width:2560px;--messenger-sidebar-collapsed-width:44px;--messenger-sidebar-expanded-width:240px;--messenger-card-corner-radius:8px;--messenger-card-spacing:16px;--chat-bubble-padding-vertical:8px;--chat-bubble-padding-horizontal:12px;--chat-bubble-border-radius:18px;--dialog-size-medium-width:100%;--dialog-size-small-width:100%;--focus-ring-outline-forced-colors:2px auto transparent !important;--focus-ring-outline-link:2px auto var(--focus-ring-blue);--focus-ring-shadow-default:0 0 0 2px var(--always-white), 0 0 0 4px var(--focus-ring-blue);--focus-ring-shadow-inset:0 0 0 2px var(--focus-ring-blue) inset, 0 0 0 4px var(--always-white) inset;--glimmer-animation-direction:alternate;--glimmer-animation-duration:1000ms;--glimmer-animation-timing-function:steps(10, end);--glimmer-opacity-min:0.25;--glimmer-opacity-max:1;--radio-checked-icon-size-large:12px;--radio-checked-icon-size-medium:12px;--radio-border-width:2px;--radio-size-large:24px;--radio-size-medium:20px;--blue-primary:rgb(0,136,244);--blue-secondary:rgb(235,245,255);--blue-tertiary:rgb(4,59,114);--chartreuse-primary:rgb(106,147,22);--chartreuse-secondary:rgb(226,255,152);--chartreuse-tertiary:rgb(50,65,19);--cyan-primary:rgb(0,142,213);--cyan-secondary:rgb(231,245,255);--cyan-tertiary:rgb(15,63,93);--dataviz-primary-2:rgb(156,219,255);--dataviz-primary-3:rgb(73,156,255);--dataviz-secondary-1:rgb(92,87,210);--dataviz-secondary-2:rgb(142,129,255);--dataviz-secondary-3:rgb(119,45,88);--dataviz-supplementary-1:rgb(253,91,67);--dataviz-supplementary-2:rgb(161,132,0);--dataviz-supplementary-3:rgb(36,131,44);--dataviz-supplementary-4:rgb(232,234,238);--fuschia-primary:rgb(250,45,138);--fuschia-secondary:rgb(255,241,246);--fuschia-tertiary:rgb(118,14,62);--green-primary:rgb(43,154,53);--green-secondary:rgb(227,250,224);--green-tertiary:rgb(26,68,27);--magenta-primary:rgb(215,77,204);--magenta-secondary:rgb(255,239,254);--magenta-tertiary:rgb(96,36,91);--orange-primary:rgb(208,108,20);--orange-secondary:rgb(255,241,239);--orange-tertiary:rgb(94,49,14);--purple-primary:rgb(125,116,255);--purple-secondary:rgb(245,241,255);--purple-tertiary:rgb(55,50,121);--red-primary:rgb(251,60,68);--red-secondary:rgb(255,241,239);--red-tertiary:rgb(118,22,27);--teal-primary:rgb(0,152,124);--teal-secondary:rgb(227,247,241);--teal-tertiary:rgb(10,68,56);--yellow-primary:rgb(161,132,0);--yellow-secondary:rgb(255,246,161);--yellow-tertiary:rgb(72,59,12);--base-unit:4px;--blue-0:245, 251, 255;--blue-1:224, 241, 255;--blue-2:179, 219, 255;--blue-3:112, 188, 255;--blue-4:71, 175, 255;--blue-5:0, 149, 246;--blue-6:0, 116, 204;--blue-7:0, 87, 163;--blue-8:0, 55, 107;--blue-9:0, 41, 82;--breakpoint-medium-width:1536px;--breakpoint-small-width:1024px;--challenge-width:460px;--creation-header-height:43px;--creation-min-padding-x:32px;--creation-modal-max-height:898px;--creation-modal-min-height:391px;--creation-padding-x:64px;--creation-padding-y:112px;--creation-settings-width:340px;--cyan-5:39, 196, 245;--desktop-grid-item-margin:28px;--desktop-grid-item-margin-slim:4px;--desktop-in-feed-story-item-height:208px;--desktop-in-feed-story-item-width:116px;--desktop-nav-height:60px;--desktop-skinny-nav-height:60px;--desktop-story-tray-bottom-margin:48px;--desktop-story-tray-bottom-margin-denser:24px;--direct-attachment-image-grid-item-size:78px;--direct-attachment-story-height:150px;--direct-attachment-story-large-height:256px;--direct-attachment-story-large-width:164px;--direct-attachment-story-width:84px;--direct-message-max-width:236px;--fb-signup-page-profile-pic-size:88px;--feed-sidebar-padding:32px;--feed-sidebar-width:319px;--feed-width:470px;--feed-width-wide-story:630px;--feed-width-wide-story-prism:702px;--font-family-system:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-family-system-prism:"Instagram Sans UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-weight-system-bold:700;--font-weight-system-extra-bold:800;--font-weight-system-extra-light:200;--font-weight-system-light:300;--font-weight-system-medium:500;--font-weight-system-regular:400;--font-weight-system-semibold:600;--footer-width-wide:1150px;--gradient-lavender:211, 0, 197;--gradient-orange:255, 122, 0;--gradient-pink:255, 1, 105;--gradient-purple:118, 56, 250;--gradient-red:255, 48, 64;--gradient-yellow:255, 214, 0;--green-4:120, 222, 69;--green-5:88, 195, 34;--green-6:55, 166, 0;--grey-0:245, 245, 245;--grey-1:239, 239, 239;--grey-2:219, 219, 219;--grey-3:199, 199, 199;--grey-4:168, 168, 168;--grey-5:142, 142, 142;--grey-6:115, 115, 115;--grey-7:85, 85, 85;--grey-8:54, 54, 54;--grey-9:38, 38, 38;--grey-10:26, 26, 26;--grey-0-prism:248, 249, 249;--grey-1-prism:243, 245, 247;--grey-2-prism:233, 237, 240;--grey-3-prism:219, 223, 228;--grey-4-prism:162, 170, 180;--grey-5-prism:131, 137, 147;--grey-6-prism:111, 118, 128;--grey-7-prism:94, 100, 109;--grey-8-prism:50, 53, 57;--grey-9-prism:41, 41, 46;--grey-10-prism:33, 35, 38;--ig-badge:255, 48, 64;--ig-close-friends-refreshed:28, 209, 79;--ig-disabled-action-text:169, 219, 255;--ig-error-or-destructive:237, 73, 86;--ig-facebook-blue:53, 121, 234;--ig-live-badge:255, 1, 105;--ig-primary-button:0, 149, 246;--ig-primary-button-hover:24, 119, 242;--ig-secondary-icon:142, 142, 142;--ig-stroke-on-media:255, 255, 255;--ig-tertiary-button-background:255, 255, 255;--ig-tertiary-button-border:219, 219, 219;--ig-tertiary-button-hover:245, 245, 245;--ig-tertiary-button-text:38, 38, 38;--ig-subscribers-only:118, 56, 250;--ig-success:88, 195, 34;--ig-text-on-color:255, 255, 255;--ig-text-on-media:255, 255, 255;--in-feed-story-item-height:240px;--in-feed-story-item-width:135px;--in-feed-story-item-padding:12px;--input-border-radius:6px;--large-layout-min:1500px;--live-video-border-radius:4px;--media-content-card-width:350px;--media-content-card-width-small:300px;--media-info:335px;--medium-layout-max:1499px;--medium-layout-min:1080px;--medium-screen-max:875px;--medium-screen-min:736px;--mobile-grid-item-margin:2px;--mobile-nav-height:45px;--modal-backdrop-dark:rgba(0, 0, 0, 0.85);--modal-backdrop-default:rgba(0, 0, 0, 0.65);--modal-border-radius:12px;--modal-padding:16px;--modal-z-index:100;--nav-narrow-width:72px;--nav-medium-width:244px;--nav-wide-width:335px;--nav-bottom-screen-max:767px;--nav-narrow-screen-min:768px;--nav-medium-screen-min:1264px;--nav-wide-screen-min:1920px;--orange-5:253, 141, 50;--photo:600px;--pink-5:209, 8, 105;--polaris-feed-with-padding-threshold-min:640px;--polaris-extra-small-screen-max:413px;--polaris-small-screen-min:414px;--polaris-small-screen-max:735px;--polaris-medium-screen-min:736px;--polaris-medium-screen-max:875px;--polaris-large-screen-min:876px;--polaris-nav-bottom-screen-max:767px;--polaris-post-modal-small-screen-max:1366px;--polaris-post-modal-large-screen-min:1536px;--polaris-post-modal-large-height-max:1032px;--polaris-challenge-width:460px;--polaris-breakpoint-medium-width:1536px;--polaris-breakpoint-small-width:1024px;--polaris-site-width-narrow:600px;--polaris-site-width-wide:935px;--polaris-site-padding-top:30px;--purple-5:163, 7, 186;--red-4:255, 104, 116;--red-5:237, 73, 86;--red-6:198, 35, 48;--red-7:167, 3, 17;--reels-large-screen-min:1366px;--refinement-section-height:50px;--revamp-nav-bottom-toolbar-height:50px;--revamp-feed-card-max-height:835px;--revamp-feed-card-min-height:615px;--revamp-feed-card-media-min-width:390px;--revamp-feed-card-dense-padding:16px;--revamp-feed-item-spacing:20px;--revamp-feed-horizontal-padding-small-screen:24px;--revamp-feed-horizontal-padding-large-screen:32px;--revamp-feed-vertical-padding:32px;--right-rail-width:300px;--scrollable-content-header-height-large:56px;--scrollable-content-header-height-med:49px;--scrollable-content-header-height:44px;--search-box-height:40px;--search-modal-height-expanded:450px;--search-modal-height:362px;--search-modal-top-offset:12px;--search-result-height:50px;--search-result-inline-top-offset:60px;--search-result-list-width:375px;--site-width-narrow:600px;--site-width-wide:935px;--small-layout-max:1079px;--small-layout-min:800px;--small-screen-max:735px;--small-screen-min:414px;--story-progressbar-update-tick:0.1s;--story-swap-animation-duration:350ms;--system-10-font-size:10px;--system-10-line-height:12px;--system-11-font-size:11px;--system-11-line-height:13px;--system-12-font-size:12px;--system-12-line-height:16px;--system-14-font-size:14px;--system-14-line-height:18px;--system-16-font-size:16px;--system-16-line-height:20px;--system-18-font-size:18px;--system-18-line-height:24px;--system-20-font-size:20px;--system-20-line-height:25px;--system-22-font-size:22px;--system-22-line-height:26px;--system-24-font-size:24px;--system-24-line-height:27px;--system-26-font-size:26px;--system-26-line-height:28px;--system-28-font-size:28px;--system-28-line-height:32px;--system-30-font-size:30px;--system-30-line-height:36px;--system-32-font-size:32px;--system-32-line-height:40px;--web-always-black:0, 0, 0;--web-always-white:255, 255, 255;--web-overlay-on-media:38, 38, 38;--web-prism-black:15, 20, 25;--web-secondary-action:224, 241, 255;--yellow-5:253, 203, 92;--barcelona-logo:255, 255, 255;--challenge-link:219, 219, 219;--ig-banner-background:38, 38, 38;--ig-elevated-background:38, 38, 38;--ig-elevated-highlight-background:54, 54, 54;--ig-elevated-separator:54, 54, 54;--ig-focus-stroke:85, 85, 85;--ig-highlight-background:38, 38, 38;--ig-hover-overlay:255, 255, 255, 0.1;--ig-link:224, 241, 255;--ig-primary-background:0, 0, 0;--ig-primary-icon:245, 245, 245;--ig-primary-text:245, 245, 245;--ig-secondary-background:26, 26, 26;--ig-secondary-button:245, 245, 245;--ig-secondary-button-background:85, 85, 85, 0.7;--ig-secondary-button-hover:54, 54, 54;--ig-secondary-text:168, 168, 168;--ig-separator:38, 38, 38;--ig-stroke:54, 54, 54;--ig-temporary-highlight:0, 149, 246;--ig-tertiary-icon:115, 115, 115;--ig-tertiary-text:115, 115, 115;--post-separator:38, 38, 38;--tos-box-shadow:255, 255, 255;--ig-toggle-background-on-prism:248, 249, 249;--ig-toggle-background-off-prism:50, 53, 57;--ig-toggle-background-disabled-on-prism:111, 118, 128;--ig-toggle-background-disabled-off-prism:41, 41, 46;--ig-toggle-outline-prism:219, 223, 228;--ig-toggle-outline-disabled-prism:94, 100, 109;--ig-stroke-prism:15, 20, 25;--ig-stroke-disabled-on-prism:50, 53, 57;--ig-stroke-disabled-off-prism:33, 35, 38;--ig-dropdown-background:41, 41, 46;}}.__ig-dark-mode {--fds-black:black;--fds-black-alpha-05:rgba(0, 0, 0, 0.05);--fds-black-alpha-10:rgba(0, 0, 0, 0.1);--fds-black-alpha-15:rgba(0, 0, 0, 0.15);--fds-black-alpha-20:rgba(0, 0, 0, 0.2);--fds-black-alpha-30:rgba(0, 0, 0, 0.3);--fds-black-alpha-40:rgba(0, 0, 0, 0.4);--fds-black-alpha-50:rgba(0, 0, 0, 0.5);--fds-black-alpha-60:rgba(0, 0, 0, 0.6);--fds-black-alpha-80:rgba(0, 0, 0, 0.8);--fds-blue-05:black;--fds-blue-30:black;--fds-blue-40:black;--fds-blue-60:black;--fds-blue-70:black;--fds-blue-80:black;--fds-button-text:black;--fds-comment-background:black;--fds-dark-mode-gray-35:black;--fds-dark-mode-gray-50:black;--fds-dark-mode-gray-70:black;--fds-dark-mode-gray-80:black;--fds-dark-mode-gray-90:black;--fds-dark-mode-gray-100:black;--fds-gray-00:black;--fds-gray-05:black;--fds-gray-10:black;--fds-gray-20:black;--fds-gray-25:black;--fds-gray-30:black;--fds-gray-45:black;--fds-gray-70:black;--fds-gray-80:black;--fds-gray-90:black;--fds-gray-100:black;--fds-green-55:black;--fds-green-65:black;--fds-highlight:black;--fds-highlight-cell-background:black;--fds-primary-icon:white;--fds-primary-text:white;--fds-red-55:black;--fds-soft:cubic-bezier(.08,.52,.52,1);--fds-spectrum-aluminum-tint-70:black;--fds-spectrum-blue-gray-tint-70:black;--fds-spectrum-cherry:black;--fds-spectrum-cherry-tint-70:black;--fds-spectrum-grape-tint-70:black;--fds-spectrum-grape-tint-90:black;--fds-spectrum-lemon-dark-1:black;--fds-spectrum-lemon-tint-70:black;--fds-spectrum-lime:black;--fds-spectrum-lime-tint-70:black;--fds-spectrum-orange-tint-70:black;--fds-spectrum-orange-tint-90:black;--fds-spectrum-seafoam-tint-70:black;--fds-spectrum-slate-dark-2:black;--fds-spectrum-slate-tint-70:black;--fds-spectrum-teal:black;--fds-spectrum-teal-dark-1:black;--fds-spectrum-teal-dark-2:black;--fds-spectrum-teal-tint-70:black;--fds-spectrum-teal-tint-90:black;--fds-spectrum-tomato:black;--fds-spectrum-tomato-tint-30:black;--fds-spectrum-tomato-tint-90:black;--fds-strong:cubic-bezier(.12,.8,.32,1);--fds-unified-blue-35:black;--fds-unified-gray-20:black;--fds-white:black;--fds-white-alpha-05:rgba(255, 255, 255, 0.05);--fds-white-alpha-10:rgba(255, 255, 255, 0.1);--fds-white-alpha-20:rgba(255, 255, 255, 0.2);--fds-white-alpha-30:rgba(255, 255, 255, 0.3);--fds-white-alpha-40:rgba(255, 255, 255, 0.4);--fds-white-alpha-50:rgba(255, 255, 255, 0.5);--fds-white-alpha-60:rgba(255, 255, 255, 0.6);--fds-white-alpha-80:rgba(255, 255, 255, 0.8);--fds-yellow-20:black;--accent:#0095F6;--always-white:white;--always-black:black;--always-dark-gradient:linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.6));--always-dark-overlay:rgba(0, 0, 0, 0.4);--always-light-overlay:rgba(255, 255, 255, 0.4);--always-gray-40:#65676B;--always-gray-75:#BCC0C4;--always-gray-95:#F0F2F5;--attachment-footer-background:rgba(255,255,255,0.1);--background-deemphasized:rgba(255,255,255,0.1);--badge-background-color-blue:var(--accent);--badge-background-color-dark-gray:var(--secondary-icon);--badge-background-color-gray:var(--disabled-icon);--badge-background-color-green:var(--positive);--badge-background-color-light-blue:var(--highlight-bg);--badge-background-color-red:var(--notification-badge);--badge-background-color-yellow:var(--base-lemon);--base-blue:#1877F2;--base-cherry:#F3425F;--base-grape:#9360F7;--base-lemon:#F7B928;--base-lime:#45BD62;--base-pink:#FF66BF;--base-seafoam:#54C7EC;--base-teal:#2ABBA7;--base-tomato:#FB724B;--text-badge-info-background:hsl(214, 100%, 59%);--text-badge-success-background:#31A24C;--text-badge-attention-background:hsl(40, 89%, 52%);--text-badge-critical-background:#e41e3f;--blue-link:#00376B;--border-focused:#8A8D91;--card-background:#242526;--card-background-flat:#323436;--comment-background:#3A3B3C;--comment-footer-background:#4E4F50;--dataviz-primary-1:rgb(0,174,143);--disabled-button-background:rgba(255, 255, 255, 0.2);--disabled-button-text:rgba(255, 255, 255, 0.3);--disabled-icon:rgba(255, 255, 255, 0.3);--disabled-text:rgba(255, 255, 255, 0.3);--divider:#3E4042;--event-date:#F3425F;--fb-wordmark:#FFFFFF;--fb-logo:#0866FF;--filter-accent:invert(40%) sepia(52%) saturate(200%) saturate(200%) saturate(200%) saturate(189%) hue-rotate(191deg) brightness(103%) contrast(102%);--filter-always-white:invert(100%);--filter-disabled-icon:invert(100%) opacity(30%);--filter-placeholder-icon:invert(59%) sepia(11%) saturate(200%) saturate(135%) hue-rotate(176deg) brightness(96%) contrast(94%);--filter-primary-accent:invert(40%) sepia(52%) saturate(200%) saturate(200%) saturate(200%) saturate(189%) hue-rotate(191deg) brightness(103%) contrast(102%);--filter-primary-icon:invert(89%) sepia(6%) hue-rotate(185deg);--filter-secondary-button-icon-on-media:invert(100%);--filter-secondary-icon:invert(62%) sepia(98%) saturate(12%) hue-rotate(175deg) brightness(90%) contrast(96%);--filter-warning-icon:invert(77%) sepia(29%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(128%) hue-rotate(359deg) brightness(102%) contrast(107%);--filter-blue-link-icon:invert(73%) sepia(29%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(103.25%) hue-rotate(189deg) brightness(101%) contrast(101%);--filter-positive:invert(37%) sepia(61%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(115%) hue-rotate(91deg) brightness(97%) contrast(105%);--filter-primary-deemphasized-button-icon:brightness(0) saturate(100%) invert(63%) sepia(20%) saturate(1290%) hue-rotate(185deg) brightness(107%) contrast(101%);--filter-negative:invert(25%) sepia(33%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(200%) saturate(110%) hue-rotate(345deg) brightness(132%) contrast(96%);--focus-ring-blue:#1877F2;--glimmer-spinner-icon:white;--hero-banner-background:#E85D07;--hosted-view-selected-state:rgba(45, 136, 255, 0.1);--highlight-bg:rgba(24, 119, 242, .31);--hover-overlay:rgba(255, 255, 255, 0.1);--inverse-text:var(--always-white);--list-cell-chevron:#B0B3B8;--media-hover:rgba(68, 73, 80, 0.15);--media-inner-border:rgba(255, 255, 255, 0.05);--media-outer-border:#33363A;--media-pressed:rgba(68, 73, 80, 0.35);--messenger-card-background:#242526;--messenger-card-box-shadow:0px 0px 16px rgba(0, 0, 0, 0.3);--mwp-header-background-color:var(--messenger-card-background);--mwp-header-button-color:var(--accent);--mwp-message-row-background:var(--messenger-card-background);--messenger-reply-background:#18191A;--overlay-alpha-80:rgba(0, 0, 0, 0.65);--overlay-on-media:rgba(0, 0, 0, 0.6);--nav-bar-background:#242526;--popover-card-background:var(--card-background);--nav-bar-background-gradient:linear-gradient(to top, #242526, rgba(36,37,38,.9), rgba(36,37,38,.7), rgba(36,37,38,.4), rgba(36,37,38,0));--nav-bar-background-gradient-wash:linear-gradient(to top, #18191A, rgba(24,25,26,.9), rgba(24,25,26,.7), rgba(24,25,26,.4), rgba(24,25,26,0));--negative:hsl(350, 87%, 55%);--negative-background:hsl(350, 87%, 55%, 20%);--new-notification-background:#E7F3FF;--non-media-pressed:rgba(68, 73, 80, 0.15);--non-media-pressed-on-dark:rgba(255, 255, 255, 0.3);--notification-badge:#e41e3f;--placeholder-icon:#8A8D91;--placeholder-image:rgb(164, 167, 171);--placeholder-text:#8A8D91;--placeholder-text-on-media:rgba(255, 255, 255, 0.5);--popover-background:#3E4042;--positive:#31A24C;--positive-background:#1F3520;--press-overlay:rgba(255, 255, 255, 0.2);--primary-button-background:#0095F6;--primary-button-icon:#FFFFFF;--primary-button-pressed:#77A7FF;--primary-button-text:#FFFFFF;--primary-deemphasized-button-background:rgba(45, 136, 255, 0.2);--primary-deemphasized-button-pressed:rgba(24, 119, 242, 0.2);--primary-deemphasized-button-pressed-overlay:rgba(25, 110, 255, 0.15);--primary-deemphasized-button-text:#2D88FF;--primary-icon:#E4E6EB;--primary-text:#E4E6EB;--primary-text-on-media:white;--primary-web-focus-indicator:#D24294;--progress-ring-neutral-background:rgba(255, 255, 255, 0.2);--progress-ring-neutral-foreground:#ffffff;--progress-ring-on-media-background:rgba(255, 255, 255, 0.2);--progress-ring-on-media-foreground:#FFFFFF;--progress-ring-blue-background:rgba(45, 136, 255, 0.2);--progress-ring-blue-foreground:hsl(214, 100%, 59%);--progress-ring-disabled-background:rgba(122,125,130, 0.2);--progress-ring-disabled-foreground:#7A7D82;--rating-star-active:#FF9831;--scroll-thumb:rgba(255, 255, 255, 0.3);--scroll-shadow:0 1px 2px rgba(0, 0, 0, 0.1), 0 -1px rgba(255, 255, 255, 0.05) inset;--secondary-button-background:rgba(255,255,255,.1);--secondary-button-background-floating:#4B4C4F;--secondary-button-background-on-dark:rgba(255, 255, 255, 0.4);--secondary-button-pressed:rgba(0, 0, 0, 0.05);--secondary-button-stroke:transparent;--secondary-button-text:#E4E6EB;--secondary-icon:#B0B3B8;--secondary-text:#B0B3B8;--secondary-text-on-media:rgba(255, 255, 255, 0.9);--section-header-text:#BCC0C4;--shadow-1:rgba(0, 0, 0, 0.1);--shadow-2:rgba(0, 0, 0, 0.2);--shadow-5:rgba(0, 0, 0, 0.5);--shadow-8:rgba(0, 0, 0, 0.8);--shadow-base:0 1px 2px var(--shadow-2);--shadow-elevated:0 8px 20px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1);--shadow-emphasis:0 2px 12px var(--shadow-2);--shadow-inset:rgba(255, 255, 255, 0.05);--shadow-on-media:0px 0px 1px rgba(0, 0, 0, 0.62);--shadow-persistent:0px 0px 12px rgba(28, 43, 51, 0.6);--shadow-primary:0px 0px 12px rgba(28, 43, 51, 0.1);--shadow-responsive:0px 8px 16px 0px rgba(0, 0, 0, 0.1);--surface-background:#242526;--switch-active:hsl(214, 100%, 59%);--switch-inactive:#3E4042;--switch-unchecked-background-color:#6F7276;--text-highlight:rgba(24, 119, 242, 0.45);--input-background:#242526;--input-background-hover:var(--input-background);--input-background-warn-hover:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-hover:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-active:hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.05);--input-background-warn-active:hsla(var(--warning-h), var(--warning-s), var(--warning-l), 0.05);--input-background-error-active:hsla(var(--negative-h), var(--negative-s), var(--negative-l), 0.05);--input-background-disabled:#18191A;--input-border-color:#3E4042;--input-border-color-hover:var(--placeholder-text);--input-label-color-highlighted:hsl(214, 100%, 59%);--input-label-color-inside:var(--secondary-text);--input-label-color-error:var(--negative);--input-label-color-disabled:var(--disabled-text);--text-input-outside-label:#FFFFFF;--toast-background:#242526;--toast-text:#FFFFFF;--toast-text-link:#4599FF;--toast-border-color:transparent;--toast-border-style:none;--toast-border-width:0px;--toggle-active-background:rgb(45, 136, 255);--toggle-active-icon:#FFFFFF;--toggle-active-text:#FFFFFF;--toggle-button-active-background:#E6F2FF;--tooltip-background:rgba(11, 11, 11, 0.8);--tooltip-box-shadow:0 2px 4px 0 var(--shadow-5);--popover-border-color:transparent;--wash:#3E4042;--web-wash:#18191A;--warning:hsl(40, 89%, 52%);--chat-bubble-emphasis-ring-after:white;--chat-bubble-emphasis-ring-before:black;--chat-text-blockquote-color-background-line:rgba(255, 255, 255, 0.19);--chat-text-blockquote-color-text-primary-media:rgba(255, 255, 255, 0.7);--chat-incoming-message-bubble-background-color:#303030;--chat-outgoing-message-bubble-background-color:var(--accent);--chat-replied-message-background-color:rgba(255, 255, 255, 0.12);--radio-border-color:var(--primary-icon);--radio-border-color-disabled:var(--disabled-button-background);--radio-border-color-selected:var(--accent);--radio-checked-icon-color:var(--accent);--radio-checked-icon-color-disabled:var(--disabled-button-background);--dialog-anchor-vertical-padding:56px;--header-height:0px;--global-panel-width:0px;--global-panel-width-expanded:0px;--alert-banner-corner-radius:8px;--button-corner-radius:4px;--button-corner-radius-medium:10px;--button-corner-radius-large:12px;--button-height-large:40px;--button-height-medium:36px;--button-padding-horizontal-large:16px;--button-padding-horizontal-medium:16px;--button-padding-icon-only:16px;--button-icon-padding-large:16px;--button-icon-padding-medium:16px;--button-inner-icon-spacing-large:3px;--button-inner-icon-spacing-medium:3px;--blueprint-button-height-medium:40px;--blueprint-button-height-large:48px;--card-corner-radius:4px;--card-box-shadow:0 12px 28px 0 var(--shadow-2), 0 2px 4px 0 var(--shadow-1);--card-padding-horizontal:10px;--card-padding-vertical:20px;--chip-corner-radius:6px;--comment-bubble:18px;--dialog-corner-radius:8px;--glimmer-corner-radius:8px;--image-corner-radius:4px;--infochip-medium-radius:12px;--input-corner-radius:6px;--input-border-width:1px;--nav-list-cell-corner-radius:8px;--list-cell-corner-radius:8px;--list-cell-min-height:52px;--list-cell-padding-vertical:20px;--list-cell-padding-vertical-with-addon:14px;--menu-base-list-item-padding-horizontal:8px;--menu-base-list-item-padding-vertical:12px;--menu-base-size-full-margin-end:48px;--menu-item-base-margin-horizontal:8px;--menu-item-base-margin-vertical:0px;--menu-item-base-padding-horizontal:8px;--menu-item-base-overlay-radius:4px;--menu-item-base-padding-vertical:12px;--menu-item-base-with-icon-padding-horizontal:var(--menu-item-base-padding-horizontal);--separator-menu-item-margin-horizontal:16px;--separator-menu-item-margin-vertical:4px;--nav-list-cell-min-height:0px;--nav-list-cell-padding-vertical:16px;--nav-list-cell-padding-vertical-with-addon:16px;--nux-card-body-padding-end:24px;--page-footer-padding-vertical:16px;--popover-border-style:none;--popover-border-width:0px;--section-header-addOnEnd-margin-horizontal:8px;--section-header-addOnStart-margin-horizontal:12px;--section-header-addOnEnd-button-padding-horizontal:0px;--section-header-addOnEnd-button-padding-vertical:0px;--section-header-padding-vertical:16px;--section-header-subtitle-margin-vertical:14px;--section-header-subtitle-with-addOnEnd-margin-vertical:6px;--tab-height:60px;--tab-icon-padding-end:0px;--tab-text-icon-gap:4px;--tab-underline-color:transparent;--tab-underline-height:3px;--text-badge-corner-radius:4px;--text-badge-padding-horizontal:6px;--text-badge-padding-vertical:6px;--text-input-multi-padding-between-text-scrollbar:20px;--text-input-multi-padding-scrollbar:16px;--text-input-caption-margin-top:10px;--text-input-padding-vertical:12px;--toast-addon-padding-horizontal:6px;--toast-addon-padding-vertical:6px;--toast-container-max-width:100%;--toast-container-min-width:288px;--toast-container-padding-horizontal:10px;--toast-container-padding-vertical:16px;--toast-corner-radius:8px;--toaster-view-max-width:328px;--tooltip-corner-radius:8px;--typeahead-list-outer-padding-vertical:2px;--fds-animation-enter-exit-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-enter-exit-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-swap-shuffle-in:cubic-bezier(0.14, 1, 0.34, 1);--fds-animation-swap-shuffle-out:cubic-bezier(0.45, 0.1, 0.2, 1);--fds-animation-move-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-move-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-in:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-expand-collapse-out:cubic-bezier(0.17, 0.17, 0, 1);--fds-animation-passive-move-in:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-passive-move-out:cubic-bezier(0.5, 0, 0.1, 1);--fds-animation-quick-move-in:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-quick-move-out:cubic-bezier(0.1, 0.9, 0.2, 1);--fds-animation-fade-in:cubic-bezier(0, 0, 1, 1);--fds-animation-fade-out:cubic-bezier(0, 0, 1, 1);--fds-duration-extra-extra-short-in:100ms;--fds-duration-extra-extra-short-out:100ms;--fds-duration-extra-short-in:200ms;--fds-duration-extra-short-out:150ms;--fds-duration-short-in:280ms;--fds-duration-short-out:200ms;--fds-duration-medium-in:400ms;--fds-duration-medium-out:350ms;--fds-duration-long-in:500ms;--fds-duration-long-out:350ms;--fds-duration-extra-long-in:1000ms;--fds-duration-extra-long-out:1000ms;--fds-duration-none:0ms;--fds-fast:200ms;--fds-slow:400ms;--font-family-apple:system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', sans-serif;--font-family-code:ui-monospace, Menlo, Consolas, Monaco, monospace;--font-family-default:Helvetica, Arial, sans-serif;--font-family-segoe:Segoe UI Historic, Segoe UI, Helvetica, Arial, sans-serif;--body-font-family:Placeholder Font;--body-font-size:0.9375rem;--body-font-weight:400;--body-line-height:1.3333;--body-emphasized-font-family:Placeholder Font;--body-emphasized-font-size:0.9375rem;--body-emphasized-font-weight:600;--body-emphasized-line-height:1.3333;--headline1-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline1-font-size:1.75rem;--headline1-font-weight:700;--headline1-line-height:1.2143;--headline2-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline2-font-size:1.5rem;--headline2-font-weight:700;--headline2-line-height:1.25;--headline3-font-family:Optimistic Display Bold, system-ui, sans-serif;--headline3-font-size:1.0625rem;--headline3-font-weight:700;--headline3-line-height:1.2941;--meta-font-family:Placeholder Font;--meta-font-size:0.8125rem;--meta-font-weight:400;--meta-line-height:1.3846;--meta-emphasized-font-family:Placeholder Font;--meta-emphasized-font-size:0.8125rem;--meta-emphasized-font-weight:600;--meta-emphasized-line-height:1.3846;--primary-label-font-family:Optimistic Display Medium, system-ui, sans-serif;--primary-label-font-size:1.0625rem;--primary-label-font-weight:500;--primary-label-line-height:1.2941;--secondary-label-font-family:Placeholder Font;--secondary-label-font-size:0.9375rem;--secondary-label-font-weight:500;--secondary-label-line-height:1.3333;--tertiary-label-font-family:Placeholder Font;--tertiary-label-font-size:0.6875rem;--tertiary-label-font-weight:500;--tertiary-label-line-height:1.4545;--text-input-field-font-family:Placeholder Font;--text-input-field-font-size:1rem;--text-input-field-font-stretch:initial;--text-input-field-font-weight:500;--text-input-field-line-height:1.2941;--text-input-label-font-family:Placeholder Font;--text-input-label-font-size:17px;--text-input-label-font-size-scale-multiplier:0.75;--text-input-label-font-stretch:initial;--text-input-label-font-weight:400;--text-input-label-line-height:1.2941;--messenger-card-min-width:300px;--messenger-card-max-width:480px;--messenger-application-max-width:2560px;--messenger-sidebar-collapsed-width:44px;--messenger-sidebar-expanded-width:240px;--messenger-card-corner-radius:8px;--messenger-card-spacing:16px;--chat-bubble-padding-vertical:8px;--chat-bubble-padding-horizontal:12px;--chat-bubble-border-radius:18px;--dialog-size-medium-width:100%;--dialog-size-small-width:100%;--focus-ring-outline-forced-colors:2px auto transparent !important;--focus-ring-outline-link:2px auto var(--focus-ring-blue);--focus-ring-shadow-default:0 0 0 2px var(--always-white), 0 0 0 4px var(--focus-ring-blue);--focus-ring-shadow-inset:0 0 0 2px var(--focus-ring-blue) inset, 0 0 0 4px var(--always-white) inset;--glimmer-animation-direction:alternate;--glimmer-animation-duration:1000ms;--glimmer-animation-timing-function:steps(10, end);--glimmer-opacity-min:0.25;--glimmer-opacity-max:1;--radio-checked-icon-size-large:12px;--radio-checked-icon-size-medium:12px;--radio-border-width:2px;--radio-size-large:24px;--radio-size-medium:20px;--blue-primary:rgb(0,136,244);--blue-secondary:rgb(235,245,255);--blue-tertiary:rgb(4,59,114);--chartreuse-primary:rgb(106,147,22);--chartreuse-secondary:rgb(226,255,152);--chartreuse-tertiary:rgb(50,65,19);--cyan-primary:rgb(0,142,213);--cyan-secondary:rgb(231,245,255);--cyan-tertiary:rgb(15,63,93);--dataviz-primary-2:rgb(156,219,255);--dataviz-primary-3:rgb(73,156,255);--dataviz-secondary-1:rgb(92,87,210);--dataviz-secondary-2:rgb(142,129,255);--dataviz-secondary-3:rgb(119,45,88);--dataviz-supplementary-1:rgb(253,91,67);--dataviz-supplementary-2:rgb(161,132,0);--dataviz-supplementary-3:rgb(36,131,44);--dataviz-supplementary-4:rgb(232,234,238);--fuschia-primary:rgb(250,45,138);--fuschia-secondary:rgb(255,241,246);--fuschia-tertiary:rgb(118,14,62);--green-primary:rgb(43,154,53);--green-secondary:rgb(227,250,224);--green-tertiary:rgb(26,68,27);--magenta-primary:rgb(215,77,204);--magenta-secondary:rgb(255,239,254);--magenta-tertiary:rgb(96,36,91);--orange-primary:rgb(208,108,20);--orange-secondary:rgb(255,241,239);--orange-tertiary:rgb(94,49,14);--purple-primary:rgb(125,116,255);--purple-secondary:rgb(245,241,255);--purple-tertiary:rgb(55,50,121);--red-primary:rgb(251,60,68);--red-secondary:rgb(255,241,239);--red-tertiary:rgb(118,22,27);--teal-primary:rgb(0,152,124);--teal-secondary:rgb(227,247,241);--teal-tertiary:rgb(10,68,56);--yellow-primary:rgb(161,132,0);--yellow-secondary:rgb(255,246,161);--yellow-tertiary:rgb(72,59,12);--base-unit:4px;--blue-0:245, 251, 255;--blue-1:224, 241, 255;--blue-2:179, 219, 255;--blue-3:112, 188, 255;--blue-4:71, 175, 255;--blue-5:0, 149, 246;--blue-6:0, 116, 204;--blue-7:0, 87, 163;--blue-8:0, 55, 107;--blue-9:0, 41, 82;--breakpoint-medium-width:1536px;--breakpoint-small-width:1024px;--challenge-width:460px;--creation-header-height:43px;--creation-min-padding-x:32px;--creation-modal-max-height:898px;--creation-modal-min-height:391px;--creation-padding-x:64px;--creation-padding-y:112px;--creation-settings-width:340px;--cyan-5:39, 196, 245;--desktop-grid-item-margin:28px;--desktop-grid-item-margin-slim:4px;--desktop-in-feed-story-item-height:208px;--desktop-in-feed-story-item-width:116px;--desktop-nav-height:60px;--desktop-skinny-nav-height:60px;--desktop-story-tray-bottom-margin:48px;--desktop-story-tray-bottom-margin-denser:24px;--direct-attachment-image-grid-item-size:78px;--direct-attachment-story-height:150px;--direct-attachment-story-large-height:256px;--direct-attachment-story-large-width:164px;--direct-attachment-story-width:84px;--direct-message-max-width:236px;--fb-signup-page-profile-pic-size:88px;--feed-sidebar-padding:32px;--feed-sidebar-width:319px;--feed-width:470px;--feed-width-wide-story:630px;--feed-width-wide-story-prism:702px;--font-family-system:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-family-system-prism:"Instagram Sans UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-weight-system-bold:700;--font-weight-system-extra-bold:800;--font-weight-system-extra-light:200;--font-weight-system-light:300;--font-weight-system-medium:500;--font-weight-system-regular:400;--font-weight-system-semibold:600;--footer-width-wide:1150px;--gradient-lavender:211, 0, 197;--gradient-orange:255, 122, 0;--gradient-pink:255, 1, 105;--gradient-purple:118, 56, 250;--gradient-red:255, 48, 64;--gradient-yellow:255, 214, 0;--green-4:120, 222, 69;--green-5:88, 195, 34;--green-6:55, 166, 0;--grey-0:245, 245, 245;--grey-1:239, 239, 239;--grey-2:219, 219, 219;--grey-3:199, 199, 199;--grey-4:168, 168, 168;--grey-5:142, 142, 142;--grey-6:115, 115, 115;--grey-7:85, 85, 85;--grey-8:54, 54, 54;--grey-9:38, 38, 38;--grey-10:26, 26, 26;--grey-0-prism:248, 249, 249;--grey-1-prism:243, 245, 247;--grey-2-prism:233, 237, 240;--grey-3-prism:219, 223, 228;--grey-4-prism:162, 170, 180;--grey-5-prism:131, 137, 147;--grey-6-prism:111, 118, 128;--grey-7-prism:94, 100, 109;--grey-8-prism:50, 53, 57;--grey-9-prism:41, 41, 46;--grey-10-prism:33, 35, 38;--ig-badge:255, 48, 64;--ig-close-friends-refreshed:28, 209, 79;--ig-disabled-action-text:169, 219, 255;--ig-error-or-destructive:237, 73, 86;--ig-facebook-blue:53, 121, 234;--ig-live-badge:255, 1, 105;--ig-primary-button:0, 149, 246;--ig-primary-button-hover:24, 119, 242;--ig-secondary-icon:142, 142, 142;--ig-stroke-on-media:255, 255, 255;--ig-tertiary-button-background:255, 255, 255;--ig-tertiary-button-border:219, 219, 219;--ig-tertiary-button-hover:245, 245, 245;--ig-tertiary-button-text:38, 38, 38;--ig-subscribers-only:118, 56, 250;--ig-success:88, 195, 34;--ig-text-on-color:255, 255, 255;--ig-text-on-media:255, 255, 255;--in-feed-story-item-height:240px;--in-feed-story-item-width:135px;--in-feed-story-item-padding:12px;--input-border-radius:6px;--large-layout-min:1500px;--live-video-border-radius:4px;--media-content-card-width:350px;--media-content-card-width-small:300px;--media-info:335px;--medium-layout-max:1499px;--medium-layout-min:1080px;--medium-screen-max:875px;--medium-screen-min:736px;--mobile-grid-item-margin:2px;--mobile-nav-height:45px;--modal-backdrop-dark:rgba(0, 0, 0, 0.85);--modal-backdrop-default:rgba(0, 0, 0, 0.65);--modal-border-radius:12px;--modal-padding:16px;--modal-z-index:100;--nav-narrow-width:72px;--nav-medium-width:244px;--nav-wide-width:335px;--nav-bottom-screen-max:767px;--nav-narrow-screen-min:768px;--nav-medium-screen-min:1264px;--nav-wide-screen-min:1920px;--orange-5:253, 141, 50;--photo:600px;--pink-5:209, 8, 105;--polaris-feed-with-padding-threshold-min:640px;--polaris-extra-small-screen-max:413px;--polaris-small-screen-min:414px;--polaris-small-screen-max:735px;--polaris-medium-screen-min:736px;--polaris-medium-screen-max:875px;--polaris-large-screen-min:876px;--polaris-nav-bottom-screen-max:767px;--polaris-post-modal-small-screen-max:1366px;--polaris-post-modal-large-screen-min:1536px;--polaris-post-modal-large-height-max:1032px;--polaris-challenge-width:460px;--polaris-breakpoint-medium-width:1536px;--polaris-breakpoint-small-width:1024px;--polaris-site-width-narrow:600px;--polaris-site-width-wide:935px;--polaris-site-padding-top:30px;--purple-5:163, 7, 186;--red-4:255, 104, 116;--red-5:237, 73, 86;--red-6:198, 35, 48;--red-7:167, 3, 17;--reels-large-screen-min:1366px;--refinement-section-height:50px;--revamp-nav-bottom-toolbar-height:50px;--revamp-feed-card-max-height:835px;--revamp-feed-card-min-height:615px;--revamp-feed-card-media-min-width:390px;--revamp-feed-card-dense-padding:16px;--revamp-feed-item-spacing:20px;--revamp-feed-horizontal-padding-small-screen:24px;--revamp-feed-horizontal-padding-large-screen:32px;--revamp-feed-vertical-padding:32px;--right-rail-width:300px;--scrollable-content-header-height-large:56px;--scrollable-content-header-height-med:49px;--scrollable-content-header-height:44px;--search-box-height:40px;--search-modal-height-expanded:450px;--search-modal-height:362px;--search-modal-top-offset:12px;--search-result-height:50px;--search-result-inline-top-offset:60px;--search-result-list-width:375px;--site-width-narrow:600px;--site-width-wide:935px;--small-layout-max:1079px;--small-layout-min:800px;--small-screen-max:735px;--small-screen-min:414px;--story-progressbar-update-tick:0.1s;--story-swap-animation-duration:350ms;--system-10-font-size:10px;--system-10-line-height:12px;--system-11-font-size:11px;--system-11-line-height:13px;--system-12-font-size:12px;--system-12-line-height:16px;--system-14-font-size:14px;--system-14-line-height:18px;--system-16-font-size:16px;--system-16-line-height:20px;--system-18-font-size:18px;--system-18-line-height:24px;--system-20-font-size:20px;--system-20-line-height:25px;--system-22-font-size:22px;--system-22-line-height:26px;--system-24-font-size:24px;--system-24-line-height:27px;--system-26-font-size:26px;--system-26-line-height:28px;--system-28-font-size:28px;--system-28-line-height:32px;--system-30-font-size:30px;--system-30-line-height:36px;--system-32-font-size:32px;--system-32-line-height:40px;--web-always-black:0, 0, 0;--web-always-white:255, 255, 255;--web-overlay-on-media:38, 38, 38;--web-prism-black:15, 20, 25;--web-secondary-action:224, 241, 255;--yellow-5:253, 203, 92;--barcelona-logo:255, 255, 255;--challenge-link:219, 219, 219;--ig-banner-background:38, 38, 38;--ig-elevated-background:38, 38, 38;--ig-elevated-highlight-background:54, 54, 54;--ig-elevated-separator:54, 54, 54;--ig-focus-stroke:85, 85, 85;--ig-highlight-background:38, 38, 38;--ig-hover-overlay:255, 255, 255, 0.1;--ig-link:224, 241, 255;--ig-primary-background:0, 0, 0;--ig-primary-icon:245, 245, 245;--ig-primary-text:245, 245, 245;--ig-secondary-background:26, 26, 26;--ig-secondary-button:245, 245, 245;--ig-secondary-button-background:85, 85, 85, 0.7;--ig-secondary-button-hover:54, 54, 54;--ig-secondary-text:168, 168, 168;--ig-separator:38, 38, 38;--ig-stroke:54, 54, 54;--ig-temporary-highlight:0, 149, 246;--ig-tertiary-icon:115, 115, 115;--ig-tertiary-text:115, 115, 115;--post-separator:38, 38, 38;--tos-box-shadow:255, 255, 255;--ig-toggle-background-on-prism:248, 249, 249;--ig-toggle-background-off-prism:50, 53, 57;--ig-toggle-background-disabled-on-prism:111, 118, 128;--ig-toggle-background-disabled-off-prism:41, 41, 46;--ig-toggle-outline-prism:219, 223, 228;--ig-toggle-outline-disabled-prism:94, 100, 109;--ig-stroke-prism:15, 20, 25;--ig-stroke-disabled-on-prism:50, 53, 57;--ig-stroke-disabled-off-prism:33, 35, 38;--ig-dropdown-background:41, 41, 46;}</style><style data-btmanifest="1021044255_main" nonce="itETU03f">@media (prefers-color-scheme: dark) {:root{background:#000;}}</style><script id="envjson" type="application/json" nonce="itETU03f">{"useTrustedTypes":false,"isTrustedTypesReportOnly":false,"routing_namespace":"igx_www","ig_server_override":"","compat_iframe_token":null,"clear_js_factory_after_used":true,"brsid":"7483563829649516416","promise_include_trace":false}</script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCnZhciBkYXRhRWxlbWVudD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiZW52anNvbiIpO2lmKGRhdGFFbGVtZW50IT1udWxsKXt2YXIgY29weVZhcmlhYmxlcz1mdW5jdGlvbihhKXtmb3IodmFyIGIgaW4gdmFyaWFibGVzKWFbYl09dmFyaWFibGVzW2JdfSx2YXJpYWJsZXM9SlNPTi5wYXJzZShkYXRhRWxlbWVudC50ZXh0Q29udGVudCk7d2luZG93LnJlcXVpcmVMYXp5P3dpbmRvdy5yZXF1aXJlTGF6eShbIkVudiJdLGNvcHlWYXJpYWJsZXMpOih3aW5kb3cuRW52PXdpbmRvdy5FbnZ8fHt9LGNvcHlWYXJpYWJsZXMod2luZG93LkVudikpfQovLyMgc291cmNlVVJMPWh0dHBzOi8vc3RhdGljLmNkbmluc3RhZ3JhbS5jb20vcnNyYy5waHAvdjQveUwvci9kYmdyTXNuRDE4cC5qcwo=" data-bootloader-hash="nxRKQO6" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9fYW5ub3RhdG9yPWZ1bmN0aW9uKGEpe3JldHVybiBhfSxfX2Rfc3R1Yj1bXSxfX2Q9ZnVuY3Rpb24oYSxiLGMsZCl7X19kX3N0dWIucHVzaChbYSxiLGMsZF0pfSxfX3JsX3N0dWI9W10scmVxdWlyZUxhenk9ZnVuY3Rpb24oKXtfX3JsX3N0dWIucHVzaChhcmd1bWVudHMpfTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy5jZG5pbnN0YWdyYW0uY29tL3JzcmMucGhwL3Y0L3kyL3IvY01kX1Nab3Y5RWkuanMK" data-bootloader-hash="W7CcKyc" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9idGxkcj17fTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy5jZG5pbnN0YWdyYW0uY29tL3JzcmMucGhwL3Y0L3lIL3IvWkxjS09iTnVPMWIuanMK" data-bootloader-hash="678jA6i" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script id="__eqmc" type="application/json" nonce="itETU03f">{"u":"\/ajax\/qm\/?__a=1&__user=0&__comet_req=7&jazoest=21016","e":"7483563829649516416","s":"XPolarisPostController","w":0,"f":null,"l":"AVqzJ_kxCRU"}</script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCnZhciBkYXRhRWxlbWVudD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiX19lcW1jIik7aWYoZGF0YUVsZW1lbnQhPW51bGwpe3ZhciBwYXJhbXM9SlNPTi5wYXJzZShkYXRhRWxlbWVudC50ZXh0Q29udGVudCksdXJpPXBhcmFtcy51LGV2ZW50X2lkPXBhcmFtcy5lLHNjcmlwdF9wYXRoPXBhcmFtcy5zLHdlaWdodD1wYXJhbXMudyxmYl9kdHNnPXBhcmFtcy5mLGxzZD1wYXJhbXMubDtpZihuYXZpZ2F0b3Iuc2VuZEJlYWNvbil7dmFyIG1hcms9ZnVuY3Rpb24oYSxiKXt2YXIgYz1wJiZwLm5vdz9NYXRoLmZsb29yKHAubm93KCkpOk1hdGgubWF4KERhdGUubm93KCktc3RhcnQsMCk7YT0iZXZlbnRfaWQ9IitldmVudF9pZCsoYT09bnVsbD8iIjoiJm1hcmtlcl9pZD0iK2EpKyImbWFya2VyX3BhZ2VfdGltZT0iK2MrIiZzY3JpcHRfcGF0aD0iK2VuY29kZVVSSUNvbXBvbmVudChzY3JpcHRfcGF0aCkrIiZ3ZWlnaHQ9Iit3ZWlnaHQrKGI/IiZjbGllbnRfc3RhcnQ9MSI6IiIpKyhmYl9kdHNnPyImZmJfZHRzZz0iK2VuY29kZVVSSUNvbXBvbmVudChmYl9kdHNnKToiIikrKGxzZD8iJmxzZD0iK2VuY29kZVVSSUNvbXBvbmVudChsc2QpOiIiKTtjPW5ldyBCbG9iKFthXSx7dHlwZToiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkIn0pO25hdmlnYXRvci5zZW5kQmVhY29uKHVyaSxjKX0sc3RhcnQ9RGF0ZS5ub3coKSxwPXdpbmRvdy5wZXJmb3JtYW5jZTttYXJrKHdlaWdodD4wPyJDbGllbnRTY3JpcHRTdGFydCI6bnVsbCwhMCk7d2VpZ2h0PjAmJndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJiZWZvcmV1bmxvYWQiLGZ1bmN0aW9uKCl7bWFyaygiQ2xpZW50VW5sb2FkIiwhMSl9KX19Ci8vIyBzb3VyY2VVUkw9aHR0cHM6Ly9zdGF0aWMuY2RuaW5zdGFncmFtLmNvbS9yc3JjLnBocC92NC95by9yL3JNalVWM3RsZzUtLmpzCg==" data-bootloader-hash="gVT/FdM" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCndpbmRvdy5vcGVuRGF0YWJhc2UmJih3aW5kb3cub3BlbkRhdGFiYXNlPWZ1bmN0aW9uKCl7dGhyb3cgbmV3IEVycm9yKCl9KTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy5jZG5pbnN0YWdyYW0uY29tL3JzcmMucGhwL3Y0L3lZL3IvOUJPeWVES0wtZEMuanMK" data-bootloader-hash="k6/EjOG" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script nonce="itETU03f" src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCihmdW5jdGlvbigpe2Z1bmN0aW9uIGEoYSl7cmV0dXJuIGEucGFyZW50RWxlbWVudCE9PWRvY3VtZW50LmJvZHkmJmEucGFyZW50RWxlbWVudCE9PWRvY3VtZW50LmhlYWR9ZnVuY3Rpb24gYihhKXtyZXR1cm4gYS5ub2RlTmFtZT09PSJTQ1JJUFQifHxhLm5vZGVOYW1lPT09IkxJTksiJiYoKGE9YyhhKSk9PW51bGw/dm9pZCAwOmEuYXN5bmNDc3MpfWZ1bmN0aW9uIGMoYSl7cmV0dXJuIShhLmRhdGFzZXQgaW5zdGFuY2VvZiB3aW5kb3cuRE9NU3RyaW5nTWFwKT9udWxsOmEuZGF0YXNldH1mdW5jdGlvbiBkKGQpe3ZhciBlO3RyeXtpZihkLm5vZGVUeXBlIT09Tm9kZS5FTEVNRU5UX05PREUpcmV0dXJufWNhdGNoKGEpe3JldHVybn1pZihhKGQpfHwhYihkKSlyZXR1cm47dmFyIGY9KGU9YyhkKSk9PW51bGw/dm9pZCAwOmUuYm9vdGxvYWRlckhhc2g7aWYoZiE9bnVsbCYmZiE9PSIiKXt2YXIgZz1udWxsLGg9ZnVuY3Rpb24oKXt3aW5kb3cuX2J0bGRyW2ZdPTEsZz09bnVsbD92b2lkIDA6ZygpfTtnPWZ1bmN0aW9uKCl7ZC5yZW1vdmVFdmVudExpc3RlbmVyKCJsb2FkIixoKSxkLnJlbW92ZUV2ZW50TGlzdGVuZXIoImVycm9yIixoKX07ZC5hZGRFdmVudExpc3RlbmVyKCJsb2FkIixoKTtkLmFkZEV2ZW50TGlzdGVuZXIoImVycm9yIixoKX19QXJyYXkuZnJvbShkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCdzY3JpcHQsbGlua1tkYXRhLWFzeW5jLWNzcz0iMSJdJykpLmZvckVhY2goZnVuY3Rpb24oYSl7cmV0dXJuIGQoYSl9KTt2YXIgZT1uZXcgTXV0YXRpb25PYnNlcnZlcihmdW5jdGlvbihhLGIpe2EuZm9yRWFjaChmdW5jdGlvbihhKXthLnR5cGU9PT0iY2hpbGRMaXN0IiYmQXJyYXkuZnJvbShhLmFkZGVkTm9kZXMpLmZvckVhY2goZnVuY3Rpb24oYSl7ZChhKX0pfSl9KTtlLm9ic2VydmUoZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoImh0bWwiKVswXSx7YXR0cmlidXRlczohMSxjaGlsZExpc3Q6ITAsc3VidHJlZTohMH0pfSkoKTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy5jZG5pbnN0YWdyYW0uY29tL3JzcmMucGhwL3Y0L3l2L3IvdHNxUDFCV0dPamMuanMK" data-bootloader-hash="YTRL02s" crossorigin="anonymous" data-p=":0" data-btmanifest="1021044255_main" data-c="1"></script><script type="application/json" data-content-len="82" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","htmlStart"]]]}</script> + +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v5/ya/l/0,cross/z8FXlCP5lIcKlxREwiTz-jcyf8QhNEWE-MbVu6J6TSgCAgbkPiBd1Gm.css" as="style" crossorigin="anonymous" /> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4/yO/r/Hk7xfAQ7REy.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4/yO/r/Hk7xfAQ7REy.js" data-bootloader-hash="g7l6vJ7" async="1" crossorigin="anonymous" data-p=":1" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4isWa4/yw/l/en_US/J5fGoS7CC80ph3lUk6VcUgI4YqdGV654wwnlKoY-CLrK92SZKLkG6-T-XrNAB1iOQtH5VMvuT83qDj1zS-v6ulv3DVZB1D_iy7Ld14PdmJWIgbxNEkM44K5ZUy9JBQP-gOz5izZwocgVl4ekeFrxv_lHobfd6yXjpTEWhHgqsu2IJGUbXmFifo4mwNXUSQN56HJIIGeUNasVCZ6IoIVGmxwaYlCzAHRsIDVc0M5Q1XPQFFb0nNwk_6eLo_DkiE3nhujbeZVC8BMrsD8q__uh0ek0AamIcMjUzA4I_q7KSGwGOomjocpx.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4isWa4/yw/l/en_US/J5fGoS7CC80ph3lUk6VcUgI4YqdGV654wwnlKoY-CLrK92SZKLkG6-T-XrNAB1iOQtH5VMvuT83qDj1zS-v6ulv3DVZB1D_iy7Ld14PdmJWIgbxNEkM44K5ZUy9JBQP-gOz5izZwocgVl4ekeFrxv_lHobfd6yXjpTEWhHgqsu2IJGUbXmFifo4mwNXUSQN56HJIIGeUNasVCZ6IoIVGmxwaYlCzAHRsIDVc0M5Q1XPQFFb0nNwk_6eLo_DkiE3nhujbeZVC8BMrsD8q__uh0ek0AamIcMjUzA4I_q7KSGwGOomjocpx.js" data-bootloader-hash="Q5jgI9M" async="1" crossorigin="anonymous" data-p=":20,52,18,51,36,47,77,50,4,59,16,15,43,73,46,48,56,60,55,38,35,53,70,19,58,113,27,29" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4iSXb4/ye/l/en_US/-iUnpybQ1ig.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4iSXb4/ye/l/en_US/-iUnpybQ1ig.js" data-bootloader-hash="TiYYQjT" async="1" crossorigin="anonymous" data-p=":14" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4i0MW4/y-/l/en_US/fyICEbODIMfh2tKnfwPYIhar4GoveLLUOA2A-x-cNjqvAz3lOgBlx2YcMH8nryRnyhLD1A0xKeUcJbZJGGdT2VN_kNrxUTVcs-o4Hw0ibRssiJqKRHNAt_rkF9mGFSb7X5HlHA9uOBNpIBFNjSuT34FTMN2lkHmdOGcu-jHRttwD3Uv_0jtI38B1Z-jjl5PRCK7W269Fr88l1ToDyBCyaJILCwuCY-bZWtS_SzTvsWqehdEtWw397qFyFcaSD9qMEkYMFZyz5yzIrtSg1gl0NX2Tg92UVk1-RfokkNf3cMKSPKuAkuWttwbwf9BVd3qCpLlkdGCSmZ.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4i0MW4/y-/l/en_US/fyICEbODIMfh2tKnfwPYIhar4GoveLLUOA2A-x-cNjqvAz3lOgBlx2YcMH8nryRnyhLD1A0xKeUcJbZJGGdT2VN_kNrxUTVcs-o4Hw0ibRssiJqKRHNAt_rkF9mGFSb7X5HlHA9uOBNpIBFNjSuT34FTMN2lkHmdOGcu-jHRttwD3Uv_0jtI38B1Z-jjl5PRCK7W269Fr88l1ToDyBCyaJILCwuCY-bZWtS_SzTvsWqehdEtWw397qFyFcaSD9qMEkYMFZyz5yzIrtSg1gl0NX2Tg92UVk1-RfokkNf3cMKSPKuAkuWttwbwf9BVd3qCpLlkdGCSmZ.js" data-bootloader-hash="fZxjt1a" async="1" crossorigin="anonymous" data-p=":62,33,21,28,26,80,61,24,71,30,98,2,95,3,32,12,22,34,40,57,68,10,7,6,13,5,11,23,8,9" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4iSC-4/yX/l/en_US/FBIYf3YthOA.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4iSC-4/yX/l/en_US/FBIYf3YthOA.js" data-bootloader-hash="eJ6xznq" async="1" crossorigin="anonymous" data-p=":31" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4is0M4/yF/l/en_US/XZJhwX0BQq_.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4is0M4/yF/l/en_US/XZJhwX0BQq_.js" data-bootloader-hash="Fo5P8JI" async="1" crossorigin="anonymous" data-p=":87" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4/y6/r/jUzyieFzD7Hg5iOb8HwiL8.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4/y6/r/jUzyieFzD7Hg5iOb8HwiL8.js" data-bootloader-hash="RBlh3hC" async="1" crossorigin="anonymous" data-p=":99,88" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4/yG/r/5sx9Xd9ch3O.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4/yG/r/5sx9Xd9ch3O.js" data-bootloader-hash="SvL53uB" async="1" crossorigin="anonymous" data-p=":83" data-btmanifest="1021044255_main" data-c="1"></script> +<link rel="preload" href="https://static.cdninstagram.com/rsrc.php/v4/yw/r/61_rxkerdqj.js" as="script" crossorigin="anonymous" nonce="itETU03f" /><script src="https://static.cdninstagram.com/rsrc.php/v4/yw/r/61_rxkerdqj.js" data-bootloader-hash="Pboc6+2" async="1" crossorigin="anonymous" data-p=":37" data-btmanifest="1021044255_main" data-c="1"></script> + + +<!--EF--> + +<title>CAPTURE THE ATLAS | ✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚 + +Last Thursday night, under the freezing skies of Northern Alaska, I... | Instagram</title> +<script type="application/json" data-content-len="34403" data-sjs>{"require":[["HasteSupportData","handle",null,[{"clpData":{"1744178":{"r":1,"s":1},"1838142":{"r":1,"s":1},"6476":{"r":1000,"s":1},"1744234":{"r":1},"1814852":{"r":1},"1828905":{"r":1},"1829320":{"r":1},"1829321":{"r":1},"1836368":{"r":1,"s":1},"1962341":{"r":1,"s":1},"29":{"r":1,"s":1},"636":{"r":1,"s":1},"1791":{"r":1},"2060":{"r":1000,"s":1},"2343":{"r":1,"s":1},"3360":{"r":1},"3707":{"r":1,"s":1},"3949":{"r":1},"3950":{"r":1},"4156":{"r":1,"s":1},"4157":{"r":1,"s":1},"4158":{"r":1,"s":1},"4159":{"r":1,"s":1},"4160":{"r":1,"s":1},"4161":{"r":1,"s":1},"4172":{"r":1,"s":1},"4173":{"r":1,"s":1},"4344":{"r":1},"4625":{"r":1,"s":1},"5537":{"r":1,"s":1},"5538":{"r":1,"s":1},"5613":{"r":1,"s":1},"5674":{"r":1},"5688":{"r":1,"s":1},"5851":{"r":1,"s":1},"5852":{"r":1,"s":1},"5853":{"r":1,"s":1},"5854":{"r":1,"s":1},"6647":{"r":1,"s":1},"6648":{"r":1,"s":1},"6661":{"r":1,"s":1},"6662":{"r":1,"s":1},"1744247":{"r":1},"1744249":{"r":1},"1837559":{"r":1},"1843988":{"r":1},"2571":{"r":1,"s":1},"6610":{"r":1,"s":1},"1746397":{"r":1,"s":1},"4530":{"r":1,"s":1},"5883":{"r":1,"s":1},"5884":{"r":1,"s":1},"5885":{"r":1,"s":1},"5886":{"r":1,"s":1},"5906":{"r":1,"s":1},"5907":{"r":1},"5908":{"r":1,"s":1},"5909":{"r":1,"s":1},"5910":{"r":1,"s":1},"5911":{"r":1,"s":1},"5912":{"r":1,"s":1},"277":{"r":1,"s":1},"497":{"r":1,"s":1},"498":{"r":1,"s":1},"499":{"r":1,"s":1},"500":{"r":1,"s":1},"541":{"r":1,"s":1},"720":{"r":1},"793":{"r":1,"s":1},"891":{"r":1},"969":{"r":1},"1055":{"r":1,"s":1},"1056":{"r":1,"s":1},"1115":{"r":1},"1255":{"r":1,"s":1},"1282":{"r":1},"1283":{"r":1},"1801":{"r":1},"1802":{"r":1},"2074":{"r":1},"2258":{"r":1},"2312":{"r":1},"2606":{"r":1,"s":1},"3085":{"r":1},"3086":{"r":1},"3087":{"r":1},"3088":{"r":1},"3089":{"r":1},"3105":{"r":1},"3175":{"r":1,"s":1},"3252":{"r":1,"s":1},"3374":{"r":1},"3447":{"r":1},"3449":{"r":1},"3628":{"r":1,"s":1},"3691":{"r":1},"3719":{"r":1,"s":1},"3989":{"r":1,"s":1},"3997":{"r":1,"s":1},"3998":{"r":1,"s":1},"3999":{"r":1,"s":1},"4000":{"r":1,"s":1},"4001":{"r":1,"s":1},"4002":{"r":1,"s":1},"4003":{"r":1,"s":1},"4004":{"r":1,"s":1},"4005":{"r":1,"s":1},"4006":{"r":1,"s":1},"4007":{"r":1,"s":1},"4008":{"r":1,"s":1},"4009":{"r":1,"s":1},"4010":{"r":1,"s":1},"4011":{"r":1,"s":1},"4012":{"r":1,"s":1},"4035":{"r":1,"s":1},"4037":{"r":1,"s":1},"4038":{"r":1,"s":1},"4039":{"r":1,"s":1},"4040":{"r":1,"s":1},"4041":{"r":1},"4042":{"r":1,"s":1},"4336":{"r":1},"4526":{"r":1},"4527":{"r":1},"4528":{"r":1},"4531":{"r":1,"s":1},"4532":{"r":1,"s":1},"4535":{"r":1},"4536":{"r":1},"5064":{"r":1},"5133":{"r":1,"s":1},"5238":{"r":1,"s":1},"5240":{"r":10000,"s":1},"5241":{"r":1,"s":1},"5242":{"r":1,"s":1},"5243":{"r":1,"s":1},"5327":{"r":1},"5481":{"r":1},"5516":{"r":1},"5517":{"r":1},"5527":{"r":1},"5533":{"r":1},"5534":{"r":1},"5612":{"r":1},"5633":{"r":1},"5634":{"r":1},"5636":{"r":1},"5703":{"r":1,"s":1},"5768":{"r":1,"s":1},"5944":{"r":1,"s":1},"5945":{"r":1,"s":1},"5946":{"r":1,"s":1},"5947":{"r":1,"s":1},"5984":{"r":1,"s":1},"5997":{"r":1,"s":1},"5998":{"r":1,"s":1},"5999":{"r":1,"s":1},"6000":{"r":1,"s":1},"6001":{"r":1,"s":1},"6002":{"r":1,"s":1},"6003":{"r":1,"s":1},"6004":{"r":1,"s":1},"6005":{"r":1,"s":1},"6006":{"r":1,"s":1},"6007":{"r":1,"s":1},"6008":{"r":1,"s":1},"6009":{"r":1,"s":1},"6010":{"r":1,"s":1},"6011":{"r":1,"s":1},"6012":{"r":1,"s":1},"6013":{"r":1,"s":1},"6014":{"r":1,"s":1},"6015":{"r":1,"s":1},"6016":{"r":1,"s":1},"6017":{"r":1,"s":1},"6018":{"r":1,"s":1},"6019":{"r":1,"s":1},"6020":{"r":1,"s":1},"1743358":{"r":1},"1743810":{"r":1},"1744524":{"r":1},"1744525":{"r":1},"1744526":{"r":1},"1744527":{"r":1},"1744552":{"r":10000},"1856513":{"r":1,"s":1},"1914651":{"r":1},"1745":{"r":1},"2686":{"r":1,"s":1},"3584":{"r":1},"5141":{"r":1,"s":1},"1860192":{"r":1,"s":1},"6508":{"r":1,"s":1},"4662":{"r":1,"s":1},"365":{"r":1,"s":1},"364":{"r":1,"s":1},"3351":{"r":1},"5864":{"r":1,"s":1}},"gkxData":{"25359":{"result":false,"hash":"AT75CT4-B9h4J50z_Rc"},"25376":{"result":false,"hash":"AT7kzFxroeRE9U689x0"},"25304":{"result":true,"hash":"AT6TxZxZFh2kzatMwCo"},"25323":{"result":false,"hash":"AT5TtmuQBzkqmJFKass"},"25343":{"result":true,"hash":"AT7hNovF0Rkb9Ua60wU"},"25344":{"result":false,"hash":"AT5sP-6HzlJypb89Rv4"},"25349":{"result":false,"hash":"AT4lNeuuoKcPqQ2xulU"},"25394":{"result":true,"hash":"AT6S3O4AIX6vJKrB0Cc"},"25408":{"result":true,"hash":"AT4zZXzD0rc8_UDg72E"},"3485":{"result":true,"hash":null},"4008":{"result":false,"hash":null},"7686":{"result":false,"hash":null},"7687":{"result":false,"hash":null},"20935":{"result":true,"hash":null},"21075":{"result":false,"hash":null},"21106":{"result":true,"hash":null},"21107":{"result":false,"hash":null},"21117":{"result":false,"hash":null},"22792":{"result":false,"hash":null},"22979":{"result":true,"hash":null},"23433":{"result":false,"hash":null},"26340":{"result":false,"hash":null},"26341":{"result":false,"hash":null},"26342":{"result":false,"hash":null},"2839":{"result":true,"hash":null},"20942":{"result":false,"hash":null},"196":{"result":false,"hash":null},"417":{"result":false,"hash":null},"1093":{"result":false,"hash":null},"1154":{"result":true,"hash":null},"1514":{"result":false,"hash":null},"1624":{"result":false,"hash":null},"2160":{"result":false,"hash":null},"2641":{"result":false,"hash":null},"5196":{"result":false,"hash":null},"5415":{"result":false,"hash":null},"5679":{"result":false,"hash":null},"6796":{"result":false,"hash":null},"7742":{"result":false,"hash":null},"8068":{"result":false,"hash":null},"8460":{"result":false,"hash":null},"8821":{"result":false,"hash":null},"8859":{"result":false,"hash":null},"8869":{"result":false,"hash":null},"9063":{"result":false,"hash":null},"9389":{"result":false,"hash":null},"10726":{"result":false,"hash":null},"10863":{"result":false,"hash":null},"10865":{"result":false,"hash":null},"20836":{"result":false,"hash":null},"20936":{"result":false,"hash":null},"20980":{"result":false,"hash":null},"20981":{"result":false,"hash":null},"20982":{"result":false,"hash":null},"20983":{"result":false,"hash":null},"20984":{"result":true,"hash":null},"20985":{"result":false,"hash":null},"20987":{"result":false,"hash":null},"20988":{"result":false,"hash":null},"21043":{"result":false,"hash":null},"21050":{"result":false,"hash":null},"21051":{"result":false,"hash":null},"21053":{"result":false,"hash":null},"21055":{"result":false,"hash":null},"21056":{"result":false,"hash":null},"21057":{"result":false,"hash":null},"21058":{"result":true,"hash":null},"22826":{"result":false,"hash":null},"22968":{"result":false,"hash":null},"8029":{"result":false,"hash":null},"6275":{"result":false,"hash":null},"22875":{"result":false,"hash":null},"22876":{"result":true,"hash":null},"329":{"result":false,"hash":null},"1534":{"result":true,"hash":null},"4064":{"result":false,"hash":null},"4161":{"result":false,"hash":null},"4252":{"result":false,"hash":null},"5247":{"result":true,"hash":null},"5258":{"result":true,"hash":null},"5303":{"result":true,"hash":null},"5607":{"result":false,"hash":null},"6375":{"result":true,"hash":null},"7024":{"result":false,"hash":null},"7604":{"result":true,"hash":null},"8570":{"result":true,"hash":null},"10182":{"result":false,"hash":null},"10233":{"result":false,"hash":null},"20921":{"result":true,"hash":null},"20922":{"result":true,"hash":null},"20923":{"result":true,"hash":null},"20924":{"result":true,"hash":null},"20925":{"result":true,"hash":null},"20926":{"result":true,"hash":null},"21003":{"result":false,"hash":null},"21004":{"result":false,"hash":null},"21005":{"result":false,"hash":null},"21089":{"result":false,"hash":null},"21095":{"result":false,"hash":null},"22681":{"result":false,"hash":null},"22803":{"result":false,"hash":null},"22877":{"result":false,"hash":null},"23406":{"result":false,"hash":null},"23415":{"result":false,"hash":null},"24215":{"result":false,"hash":null},"24263":{"result":false,"hash":null},"4150":{"result":false,"hash":null},"10674":{"result":false,"hash":null},"340":{"result":false,"hash":null},"579":{"result":false,"hash":null},"1196":{"result":false,"hash":null},"1605":{"result":false,"hash":null},"1667":{"result":false,"hash":null},"2345":{"result":false,"hash":null},"2887":{"result":true,"hash":null},"3190":{"result":false,"hash":null},"4867":{"result":false,"hash":null},"5380":{"result":false,"hash":null},"5620":{"result":false,"hash":null},"6181":{"result":false,"hash":null},"6536":{"result":false,"hash":null},"6626":{"result":false,"hash":null},"6676":{"result":false,"hash":null},"6708":{"result":false,"hash":null},"8772":{"result":false,"hash":null},"10390":{"result":false,"hash":null},"25336":{"result":false,"hash":null},"25377":{"result":false,"hash":null},"25386":{"result":false,"hash":null},"25392":{"result":false,"hash":null},"32992":{"result":false,"hash":null},"32993":{"result":false,"hash":null},"32994":{"result":false,"hash":null},"33000":{"result":false,"hash":null},"33001":{"result":false,"hash":null},"33060":{"result":true,"hash":null},"4737":{"result":false,"hash":null},"7170":{"result":false,"hash":null},"20864":{"result":true,"hash":null},"20865":{"result":false,"hash":null},"23413":{"result":false,"hash":null},"23414":{"result":false,"hash":null},"366":{"result":false,"hash":null},"3399":{"result":false,"hash":null},"4516":{"result":false,"hash":null},"10501":{"result":false,"hash":null},"10725":{"result":false,"hash":null},"20858":{"result":false,"hash":null},"20860":{"result":false,"hash":null},"20863":{"result":false,"hash":null},"1326":{"result":false,"hash":null},"2442":{"result":false,"hash":null},"3123":{"result":true,"hash":null},"174":{"result":true,"hash":null},"3129":{"result":false,"hash":null},"3133":{"result":false,"hash":null},"3353":{"result":false,"hash":null},"32979":{"result":false,"hash":null},"24":{"result":true,"hash":null},"44":{"result":false,"hash":null},"440":{"result":false,"hash":null},"760":{"result":false,"hash":null},"1008":{"result":false,"hash":null},"1211":{"result":true,"hash":null},"1317":{"result":false,"hash":null},"1810":{"result":true,"hash":null},"2322":{"result":false,"hash":null},"2400":{"result":false,"hash":null},"2406":{"result":false,"hash":null},"2553":{"result":false,"hash":null},"2750":{"result":false,"hash":null},"3179":{"result":false,"hash":null},"3356":{"result":false,"hash":null},"4192":{"result":false,"hash":null},"4559":{"result":false,"hash":null},"4757":{"result":false,"hash":null},"4760":{"result":false,"hash":null},"5043":{"result":false,"hash":null},"5268":{"result":false,"hash":null},"5551":{"result":false,"hash":null},"5608":{"result":false,"hash":null},"5688":{"result":false,"hash":null},"5712":{"result":true,"hash":null},"5923":{"result":false,"hash":null},"6169":{"result":false,"hash":null},"6650":{"result":false,"hash":null},"6934":{"result":false,"hash":null},"7221":{"result":false,"hash":null},"7396":{"result":false,"hash":null},"7608":{"result":true,"hash":null},"8251":{"result":false,"hash":null},"8287":{"result":false,"hash":null},"8409":{"result":false,"hash":null},"8610":{"result":false,"hash":null},"9104":{"result":false,"hash":null},"9144":{"result":false,"hash":null},"9551":{"result":true,"hash":null},"9859":{"result":false,"hash":null},"9965":{"result":false,"hash":null},"10027":{"result":true,"hash":null},"10278":{"result":false,"hash":null},"11001":{"result":false,"hash":null},"11003":{"result":false,"hash":null},"11135":{"result":false,"hash":null},"11189":{"result":false,"hash":null},"11208":{"result":false,"hash":null},"20941":{"result":false,"hash":null},"21000":{"result":true,"hash":null},"21116":{"result":false,"hash":null},"22879":{"result":false,"hash":null},"23157":{"result":false,"hash":null},"23174":{"result":false,"hash":null},"23176":{"result":false,"hash":null},"23177":{"result":false,"hash":null},"23178":{"result":false,"hash":null},"23179":{"result":false,"hash":null},"23180":{"result":false,"hash":null},"23181":{"result":false,"hash":null},"23182":{"result":false,"hash":null},"23184":{"result":false,"hash":null},"23185":{"result":false,"hash":null},"23186":{"result":false,"hash":null},"23187":{"result":false,"hash":null},"23188":{"result":false,"hash":null},"23189":{"result":false,"hash":null},"23190":{"result":false,"hash":null},"23191":{"result":false,"hash":null},"23192":{"result":false,"hash":null},"23193":{"result":false,"hash":null},"23195":{"result":false,"hash":null},"23196":{"result":false,"hash":null},"23197":{"result":false,"hash":null},"23198":{"result":false,"hash":null},"23199":{"result":false,"hash":null},"23200":{"result":false,"hash":null},"23201":{"result":false,"hash":null},"23202":{"result":false,"hash":null},"23203":{"result":false,"hash":null},"23204":{"result":true,"hash":null},"23208":{"result":false,"hash":null},"23209":{"result":false,"hash":null},"23210":{"result":false,"hash":null},"24278":{"result":true,"hash":null},"24328":{"result":false,"hash":null},"24329":{"result":false,"hash":null},"24330":{"result":false,"hash":null},"24342":{"result":false,"hash":null},"24344":{"result":false,"hash":null},"24345":{"result":true,"hash":null},"24349":{"result":false,"hash":null},"24350":{"result":false,"hash":null},"24351":{"result":false,"hash":null},"24360":{"result":false,"hash":null},"24377":{"result":true,"hash":null},"24380":{"result":false,"hash":null},"24382":{"result":false,"hash":null},"24383":{"result":false,"hash":null},"25340":{"result":false,"hash":null},"25345":{"result":true,"hash":null},"25346":{"result":false,"hash":null},"25348":{"result":false,"hash":null},"25355":{"result":true,"hash":null},"25360":{"result":false,"hash":null},"25368":{"result":false,"hash":null},"25390":{"result":false,"hash":null},"25403":{"result":false,"hash":null},"25417":{"result":false,"hash":null},"26216":{"result":false,"hash":null},"30214":{"result":false,"hash":null},"32990":{"result":false,"hash":null},"33068":{"result":false,"hash":null},"1704":{"result":false,"hash":null},"9690":{"result":false,"hash":null},"11237":{"result":false,"hash":null},"226":{"result":false,"hash":null},"1551":{"result":false,"hash":null},"4871":{"result":false,"hash":null},"21118":{"result":false,"hash":null},"22787":{"result":false,"hash":null},"22809":{"result":true,"hash":null},"22874":{"result":false,"hash":null},"24381":{"result":false,"hash":null},"25293":{"result":false,"hash":null},"26335":{"result":false,"hash":null},"26336":{"result":false,"hash":null},"26337":{"result":false,"hash":null},"4070":{"result":false,"hash":null},"23403":{"result":false,"hash":null},"23404":{"result":false,"hash":null},"8049":{"result":false,"hash":null},"8708":{"result":false,"hash":null},"9861":{"result":true,"hash":null},"10211":{"result":false,"hash":null},"10662":{"result":false,"hash":null},"10839":{"result":true,"hash":"AT5rFz3dcDqbx7eNdHg"},"10850":{"result":false,"hash":null},"21063":{"result":false,"hash":null},"21069":{"result":false,"hash":null},"21071":{"result":false,"hash":null},"21072":{"result":false,"hash":null},"33056":{"result":false,"hash":null},"1709":{"result":true,"hash":null},"23999":{"result":false,"hash":null},"4927":{"result":false,"hash":null},"10592":{"result":false,"hash":null},"10769":{"result":false,"hash":null},"24353":{"result":false,"hash":null},"24354":{"result":false,"hash":null},"24355":{"result":false,"hash":null},"24356":{"result":false,"hash":null},"25572":{"result":false,"hash":null}},"ixData":{"1876411":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yd\/r\/3mD7kKai_7W.gif","width":12,"height":12},"1876412":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y1\/r\/mHADa0fT0mI.gif","width":16,"height":16},"1876413":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/ZY0eC865SgX.gif","width":20,"height":20},"1876414":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y8\/r\/M3mvaC7u8oH.gif","width":24,"height":24},"1876415":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yL\/r\/hVe2HmwMRpE.gif","width":32,"height":32},"1876416":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/yFaaylccZ5L.gif","width":48,"height":48},"1876418":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yk\/r\/96GJYGbUDCJ.gif","width":72,"height":72},"1876419":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yS\/r\/NiR8M1k4AVU.gif","width":12,"height":12},"1876420":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ym\/r\/FNERtXIk9xp.gif","width":16,"height":16},"1876421":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yJ\/r\/l2FWxc8ihQj.gif","width":20,"height":20},"1876422":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/Io_N1z4MXYh.gif","width":24,"height":24},"1876423":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yD\/r\/-1hifBvDgEQ.gif","width":32,"height":32},"1876424":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/RcIiVWWukEr.gif","width":48,"height":48},"1876426":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yP\/r\/9ISCYYcy94m.gif","width":72,"height":72},"1876427":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yU\/r\/MStXnCtsaSe.gif","width":12,"height":12},"1876428":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/dw2egiKdoVV.gif","width":16,"height":16},"1876429":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yA\/r\/1DbfjOftY0d.gif","width":20,"height":20},"1876430":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yi\/r\/2uPGz8a6lb6.gif","width":24,"height":24},"1876431":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yw\/r\/60r9oPEvxiL.gif","width":32,"height":32},"1876432":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yM\/r\/NlAFhiEx3a1.gif","width":48,"height":48},"1876434":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yx\/r\/uzrQzxgD_Bg.gif","width":72,"height":72},"1876435":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yM\/r\/TtXj9IXnkoK.gif","width":12,"height":12},"1876436":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yy\/r\/HNs8yq0QiXE.gif","width":16,"height":16},"1876437":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yT\/r\/ay_drQe6StD.gif","width":20,"height":20},"1876438":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y4\/r\/iACDMhAROS_.gif","width":24,"height":24},"1876439":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yy\/r\/WEhNL1y2zoZ.gif","width":32,"height":32},"1876440":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/mAeZkO4yhqj.gif","width":48,"height":48},"1876442":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/79uB7ciX8vY.gif","width":72,"height":72},"1876443":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/dzn6it4Fw3p.gif","width":12,"height":12},"1876444":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yc\/r\/wqjQpFb4tea.gif","width":16,"height":16},"1876445":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yk\/r\/yy3mR2PXKrn.gif","width":20,"height":20},"1876446":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/gTdm7zPKz-c.gif","width":24,"height":24},"1876447":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ym\/r\/kdaft251gQ_.gif","width":32,"height":32},"1876448":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/6-FTd4KBtOk.gif","width":48,"height":48},"1876450":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yq\/r\/Tks_lRPtYc-.gif","width":72,"height":72},"1876451":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/Bys0xcVibDa.gif","width":12,"height":12},"1876452":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yI\/r\/Wk0dcHGH6EG.gif","width":16,"height":16},"1876453":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ys\/r\/aOTs7vt2hEc.gif","width":20,"height":20},"1876454":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yh\/r\/wVjfNbGZ3CH.gif","width":24,"height":24},"1876455":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yw\/r\/oT6wM_vuQNQ.gif","width":32,"height":32},"1876456":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yp\/r\/ac61i44rSWK.gif","width":48,"height":48},"1876458":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yg\/r\/ZH27Vvjc9-u.gif","width":72,"height":72},"1940508":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y3\/r\/ycQ2OPoZwUA.gif","width":64,"height":64},"1940509":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yg\/r\/8kyIVWHZW-b.gif","width":64,"height":64},"1940510":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/8gPN8wBD9yB.gif","width":64,"height":64},"1940511":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/WtK_u51t3nM.gif","width":64,"height":64},"1940512":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yj\/r\/JYwEre3ewp7.gif","width":64,"height":64},"1940513":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yu\/r\/M2HDLLPAUWl.gif","width":64,"height":64},"502062":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-164px -97px","sz":"auto"},"225223":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":96,"h":96,"p":"0 -97px","sz":"auto"},"225234":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":96,"h":96,"p":"0 -194px","sz":"auto"},"162687":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":192,"h":96,"p":"0 0","sz":"auto"},"166735":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":16,"h":16,"p":"0 -200px","sz":"auto"},"184566":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -100px","sz":"auto"},"484863":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-139px -122px","sz":"auto"},"497675":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-118px -122px","sz":"auto"},"534219":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-160px -122px","sz":"auto"},"1296473":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -25px","sz":"auto"},"1346159":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -50px","sz":"auto"},"1350889":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 0","sz":"auto"},"1356754":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -75px","sz":"auto"},"1361369":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -175px","sz":"auto"},"1362297":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":16,"h":16,"p":"0 -217px","sz":"auto"},"1729923":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -125px","sz":"auto"},"1795210":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/4Q1ivFeaK14.png","w":24,"h":24,"p":"0 -150px","sz":"auto"},"163346":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":56,"h":56,"p":"0 -1051px","sz":"auto"},"841170":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 0","sz":"auto"},"841172":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -97px","sz":"auto"},"841174":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -194px","sz":"auto"},"841176":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -291px","sz":"auto"},"841178":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -388px","sz":"auto"},"841180":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -485px","sz":"auto"},"841182":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -582px","sz":"auto"},"841184":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -679px","sz":"auto"},"841186":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -776px","sz":"auto"},"841188":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":192,"h":96,"p":"0 -873px","sz":"auto"},"879111":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":80,"h":80,"p":"0 -970px","sz":"auto"},"879114":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/EuEooaq6QG5.png","w":64,"h":64,"p":"-81px -970px","sz":"auto"},"478233":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-97px -122px","sz":"auto"},"512647":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-122px -97px","sz":"auto"},"514454":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":20,"h":20,"p":"-143px -97px","sz":"auto"},"879116":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/02VXQUgSY8Z.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/02VXQUgSY8Z.png","w":45,"h":45,"p":"0 -65px","sz":"auto"},"879118":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/02VXQUgSY8Z.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/02VXQUgSY8Z.png","w":64,"h":64,"p":"0 0","sz":"auto"},"478231":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":12,"h":12,"p":"-97px -143px","sz":"auto"}},"metaconfigData":{"73":{"value":false},"143":{"value":false},"179":{"value":false},"3":{"value":false}},"qexData":{"1696":{"r":null},"354":{"r":null},"494":{"r":false},"751":{"r":false},"889":{"r":false},"14":{"r":false},"103":{"r":false},"324":{"r":false},"365":{"r":null},"611":{"r":true,"l":"J{\"qeid\":\"CD5B157A-74AB-4D01-BA90-0939AF84E3DB\",\"u\":\"\",\"t\":\"ig_web_device_id\",\"gks\":[],\"qe\":null}"},"881":{"r":null},"1003":{"r":false},"1250":{"r":false},"1483":{"r":false},"1683":{"r":false},"1701":{"r":0},"1733":{"r":false},"1838":{"r":false},"1844":{"r":true},"3198":{"r":null},"3199":{"r":null},"3243":{"r":false},"3265":{"r":false},"37":{"r":null,"l":"J{\"qeid\":\"CD5B157A-74AB-4D01-BA90-0939AF84E3DB\",\"u\":\"\",\"t\":\"ig_web_device_id\",\"gks\":[],\"qe\":\"2818015915036730\"}"},"82":{"r":null},"206":{"r":null},"252":{"r":null},"659":{"r":true,"l":"J{\"qeid\":\"CD5B157A-74AB-4D01-BA90-0939AF84E3DB\",\"u\":\"\",\"t\":\"ig_web_device_id\",\"gks\":[],\"qe\":null}"},"748":{"r":null},"821":{"r":true},"1402":{"r":true,"l":"J{\"qeid\":\"CD5B157A-74AB-4D01-BA90-0939AF84E3DB\",\"u\":\"\",\"t\":\"ig_web_device_id\",\"gks\":[],\"qe\":null}"},"1488":{"r":null},"3458":{"r":true},"6":{"r":null},"121":{"r":null},"169":{"r":null},"246":{"r":null},"295":{"r":null},"308":{"r":null},"334":{"r":null},"343":{"r":null},"357":{"r":null},"384":{"r":null},"647":{"r":false},"662":{"r":null},"714":{"r":null},"954":{"r":null},"1229":{"r":null},"1279":{"r":null},"1346":{"r":null},"1542":{"r":null},"1556":{"r":null},"1586":{"r":null},"1617":{"r":null},"1655":{"r":null},"1656":{"r":null},"1675":{"r":null},"2021":{"r":null},"2060":{"r":null},"3010":{"r":null},"3113":{"r":null},"3128":{"r":null},"3139":{"r":true},"3144":{"r":null},"3292":{"r":true},"3392":{"r":null},"3501":{"r":null},"447":{"r":false},"471":{"r":null},"1251":{"r":false},"3515":{"r":false},"314":{"r":false},"548":{"r":false},"1849":{"r":false},"2158":{"r":false},"13":{"r":null},"639":{"r":null},"1492":{"r":0},"1524":{"r":null},"1657":{"r":false},"104":{"r":null},"128":{"r":null},"344":{"r":null},"388":{"r":null},"1636":{"r":null}},"qplData":{"194":{"r":1},"685":{"r":1},"2410":{"r":1},"2702":{"r":1},"6204":{"r":7},"6702":{"r":1},"21":{"r":1},"192":{"r":2000},"334":{"r":2000},"1389":{"r":2000},"1597":{"r":1},"1885":{"r":1},"6287":{"r":1},"6915":{"r":1},"8327":{"r":1},"2242":{"r":10000},"2258":{"r":1000000},"2267":{"r":10000},"2268":{"r":10000},"2396":{"r":10000},"2408":{"r":10000},"4750":{"r":1},"1411":{"r":100},"299":{"r":10},"720":{"r":10000},"749":{"r":10000},"758":{},"932":{"r":1},"1014":{"r":10000},"1111":{"r":10},"1208":{},"1257":{"r":10000},"1261":{"r":10000},"1297":{"r":10000},"1298":{},"1610":{"r":1},"1611":{"r":1},"1612":{"r":1},"1722":{"r":10},"1723":{"r":10},"1890":{"r":10000},"2144":{"r":1},"2371":{"r":1},"2401":{"r":5},"955":{"r":1},"1360":{"r":100},"1388":{"r":1},"2172":{},"3796":{"r":1},"5252":{"r":1}},"justknobxData":{"1806":{"r":true},"2635":{"r":true},"2269":{"r":true},"3323":{"r":true},"144":{"r":true},"450":{"r":true},"1417":{"r":true},"3409":{"r":true},"494":{"r":true},"674":{"r":true},"957":{"r":false},"1849":{"r":true},"2349":{"r":false},"2552":{"r":true},"2743":{"r":true},"3457":{"r":true},"3478":{"r":true},"3805":{"r":true},"3809":{"r":true},"76":{"r":false},"82":{"r":true},"306":{"r":true},"380":{"r":true},"643":{"r":true},"1126":{"r":true},"1413":{"r":true},"1540":{"r":true},"1980":{"r":true},"2750":{"r":true},"2928":{"r":false},"3076":{"r":true},"3204":{"r":true},"3756":{"r":true},"87":{"r":true},"3146":{"r":true},"64":{"r":false},"73":{"r":25000},"89":{"r":false},"90":{"r":true},"130":{"r":true},"222":{"r":true},"341":{"r":false},"432":{"r":true},"589":{"r":true},"592":{"r":11},"593":{"r":11},"604":{"r":false},"669":{"r":true},"684":{"r":true},"706":{"r":false},"783":{"r":false},"794":{"r":false},"843":{"r":false},"922":{"r":false},"931":{"r":false},"995":{"r":true},"1028":{"r":false},"1051":{"r":false},"1070":{"r":false},"1118":{"r":false},"1141":{"r":true},"1175":{"r":false},"1192":{"r":false},"1211":{"r":false},"1212":{"r":false},"1564":{"r":true},"1641":{"r":true},"1731":{"r":2},"1749":{"r":false},"1831":{"r":10},"1832":{"r":100},"1998":{"r":true},"1999":{"r":false},"2000":{"r":false},"2004":{"r":false},"2217":{"r":true},"2382":{"r":true},"2436":{"r":10},"2437":{"r":50},"2450":{"r":true},"2644":{"r":true},"3326":{"r":true},"3332":{"r":false},"3337":{"r":false},"3371":{"r":true},"3604":{"r":false},"3609":{"r":true},"3693":{"r":3},"3694":{"r":10},"3714":{"r":66},"3752":{"r":false},"3846":{"r":true},"3847":{"r":true},"653":{"r":20000},"757":{"r":false},"138":{"r":false},"356":{"r":true},"713":{"r":true},"887":{"r":true},"1759":{"r":true},"3727":{"r":true},"55":{"r":true},"2233":{"r":true},"717":{"r":true}}}]]]}</script> +</head><body class="_ar45 system-fonts--body"><div id="splash-screen" style="position:fixed;z-index:100;top:0;left:0;width:100%;height:100%"><style type="text/css" nonce="itETU03f">html{overflow-y:scroll!important}</style><img width="80px" height="80px" style="position:absolute;top:50%;left:50%;margin:-40px 0 0 -40px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAtXklEQVR4AezBAQ0AAAjAoNu/tDl0QAAAAAAAAG9NRy179gDrX9NdcfyzZ8793b9r27Zt2wqqoHYb1rYdt1Hj1AhqI6jdPrafizO7uHOSnfMi+uvFTlZm73XO9TdrZnK/TB6/Ctef5jqudm4MbgTXBofGUXAYHJaLvq/GUee4GR2tGz2tndaa80a2hWhAdCPCiAA0dBmBBQ1bH4BONqFJi8ggm6FrGWRnXKwxmrHS1kWsYYyDOBucdLk27Sycnod2cixOhzw7Fs8NnljFk8PZ01fFs087fRzPegmt+HP3uNvrF7325eD1lu6t1/SW2bxWX71x8iqRDskRjoNlqk9FoyW9yUjZQvYwgmxdIqMRzRoNNuiEjmYIIqQmNQQaltJvft/gFLnNDX3OIbLJ7FKIPBIjZAbTjzUYTeairawjGOToYm2chHG6cIa1y8eSe7r492Y8EM7/7Ez7rzf1mg8j3eUVKd1N9ZVXvUY786qteUfpnYKPkN5QuhaJokgCEEUNCbM34QlDQxhCCpu/gjb9xtTQUOHr6ucMCzbfXKN4Md+pMIbQ6rtzbnLO1Ln21QspdjPDBaT53138NeN3mvO/Pji+7xW95n04eTmAu/o8Lr3qVW++nnmj1ny08B6R3ly6DJEYhLnu4bPv9zCOCVAW+NjgaxID6QghN1AnHEOvMBTgluLt4eo28KI+n37Od7MAOHQNSQGvzb4CGGWtgDLUPlg79zTrn5G/162//4z1P/Cwu6DiHv/gTtYPXXnzj8nwYdL7S6/b0qtFIIkJm6TC15JaHQqE6h+spF2Xcs6b39VkTItkex/toi/gbT52oDW0CZitl5svsGy9nE5qWMQOMKImq6SAW1O5wvjCNbbPfTqs/975S/JXbzj/VTxyhxPw79yJ+porb/lOwee15qMNryNdikQWgCaEkpizCWAgC2hKH1BTrEC1zNSLDawCYNsDKAuYUYBlQYgKjIOgAlfTUmyfRxdy9il1sYdOg33i1c+/B466Fo0pQnIxn+L+kP/cjB++4t9+Eyd3CMBfdbvqy33U8ZVr3uucL4z0icHV2GCaYBlAywLclKyAAYGOKGogS/LVs9+qTfBM4FqBsU3IzHWhwBuipGDIOdNFScU9gAG7rbSeA4HYVAGsniFEAW0gNCkk068/nwmhzZt92a7PG3966vQHw8kf4H63seJpf+p21Ld4t9d03cdKXxe8uQRiEGqylXMeYi3PVQBfeGLsz0nNqtU0nOnX59z3aWctZ7cJZwGws9/6LKKAVS8eKbQdTKFJrSTh5sf0a5rZIBUlybBPPpA7GPdKrAIqjFKzPtT4le78m/EftzEB/9KtrPBO8Q3XvLX0tcJH49UlMWjq9nohaGP6VfUP/GLUqYmmGxiaUdJt1LSbIG694pkfF/UWLMo2u78Zd20DYoNM7OA0e+X2awO39HsQ61wvUknxxsTa7JXkI6zswdxAtEr5W+SPd9d/43ZsywvX3cr62uveE98ZvLdwsMHUaImYwAUxoIJHY5cEBKAVlWQoyTV0Y65ZUs0GqAWBmLAu5VmBtMC9hzxmHxobQOy2zQ3YCiepUdIaCPbPZIFYgc8ujbMmIxW0zdGo6VfW0OQH4fV48rVYfgHP3OIE/Bu3or7a218+vuGTpZ8P84JRoAJoFbpBjN35zy79CgTFL/2oAAprmdOypSETsGGRYurIoKRdLynSRAVYF0xvQlWSDxtYWgEidhenALETkPXzU8BUkpDcz4LiZz0DCuaawpBztUlOnf8SR1+Mh29hAp7dgvPeu1w5vILPaekbpEsRBCJQt9S5GrsLRdIrpFDhm4K2B3N3QyypNZUl8WYKTmjJAtiw1PcpICsA24ShFcBil3pRe+qtlro9S9if7+TuZ8MuxVRApdwl6/Y5kqKo0FH6+DTO/pHTn8F/3RIAb3bCfu+rvv/10xOfsaRvD149AogoIGUBcqWNDcjZ7858ClgNSt/ZATGmN0aT502eLPL5Lk/TOFt4Pi78sRgZcjTySGaTSbrw6bJAPdLs65bbNGFomkZJGtGY7wUiJkzRBTQyQosmG9nJQ4hLF2seB5dCLEFLIwJ250AAsgK+AwwGCLn1yJ0A9R3rVxOvyvrt+O9bAOBwM+uZUx/bYsKHTZIAtCAqaBt8Zd4lWvXOgoeDexb+Jbmn8dTCk8RzXTzfrKchnj/iJLVnunyuy/O0JrkGo4tMq8vkKZCLwBlIHEgO4OAUADFdDhKBVIuDI8Cp0zhsroMzZ3E014OG8yDi3HkPrR85tHB6vFquNC43ebxwPOQxcSXEFdZXaNorhnxL8nXxWuRlYBQIYZQ+i/aVRYGBuIxP4+ghjn4Yj7iJFelP3Kz6thvv/m5n/FGjB5Rz3P4/Gm0q7NOvbquexn0L/xn8Ef7mwJ/8J/dgeHkB+Cb/8uosb8r6/ng3vBnj1cnXICuMU1l6Zc6ptXjrXP0wH/h1OL/r/hf8Lde8re6HpA+NknaSmDJoCmQD6/RMKP+nvbsAsuS4En7/y6y63T2sscVMRsmklekz4zIzM++alzf8DIvGb3ntZd41My2ZmS1ZMglHjMPdfavyPLobcSNj6k231Hem50X/I9JZWdcjaWb+fbLOqcy8RMMefBIfSfxnw6dX/iC8wU+5fjMH70d5IL6HuBf9mUQzLeNEsqlWKumgn5a2p3sq/msNBXznGsj31BOb7EWJ74iwJcHUtBpBiinJpt9wFGIiZmY586+JdxY+cCs3YNkGdyMyfuJ4tj6A7htI3045qxKuuu6hjnwIdCifoPwsPrZGAr7d3eGnfc3o9O2+PfOX2JICpoUjBYFcJRmCpp+IyheDP5znjTu4Ab01YYPvJHHJiYzOZPnFlMfhUBJOtXock9YvEf9G+yzctgYC/oe7w28d96THN+HvU3FGSvifCAf/IyDERLhCiknrydyY+MfEi3GzmbLBz/jsFnwH/dOIB1JawnSjryJifd9BumfR/A2W3A3auzvDjXrflzgjBSYt+Z+ePB0Ro46Cbiv8Pv4y2GfmbPCnHrj/5/h7Pn496bnEo1FFvPj/aFA2kX6Cuffi80ctAr5k+5MeGsV7EpvS/0gGUMiTPiFNop9g8hy4C7/1U7wSYYOjwCfuS/+PlK869FQcA9lxZ3L9J/gFd4OWPe4qUfxEEzZJ1AJCE5QgIwc5kXqasBsvnecf/4GwwVHiqy7/QR/6Tfwu8WCmZQMD2XEgkL6XO5+Dxbsh4G6snpct/MgT5nhiQhRSIiEVpmUc1e96w0147k/wFwgbHGUe+Q4+fAfxvykPIzI9oAy0MOnvwfZn8u2/h7B6pLB6Xi3mbtrs5Sn5sQgLSbWkatKmi8uTscQf7eZXcNCas8Ezvbvl8QXFqnj3D5BeTjkeKAh0iCEBUT7M8ndil9Wj5R+tlju2uE/LIxULkKaXyBcgFUzX/oAvBr+zc83k2+BHvLvl+LPIj6C7F8edxqfvIK4gfwSfsiJueS0n3Jf4jfr57zDlmvNpvwZ/afVoaa2W1Dmn4XyJBIjpwnKaqgGaXBN4GW60hmxwwq9RvonxaaQt2ExZxh7KbYxfQ/c3uNowOO0gy39M/AJlx7CEdYISx1Eexc5/wNLqBbTdavhLX3vSiG+IsC0FEUAytZOtLsEg8erCW91tNvh+Ep+9AC8mvoaAaTFaYjNOJi6g+S76X2P5nVg0zI2MX0D8LjFHDwiUqkFB1+Iibn0wPrJ6Ad1qNcxxai4eHQGYFhASqQDQIrE38/pXcYM1YEPAz56G36U8GXWNDuoodT/S8xjdjvcxDPvfTvP9lItI9T8HtYSBOBfn88MfRaxSwB9cnYCNe0fvPEFKlCDXElKv5/vomA98G70N7hbf4/072fKDpKcQLQWgFqNuD6a8BA+HYfIV9K+iPIho6BEDq2QAbKW/iL99K+5cpYB/azXk/kceHswlCDIyYmB7IQ4kPnEJ11kDNthxOuMfJOZRiRCAGGpfRX4gPmuQrUvc8UnKtTi7WpwKCEBfCT4+EXfOdEn+HF8TyAAIqASckvLaxFsuplgDNlj8avL9KIgB6aD+HEpD+Wme8AsIg/zrpcxfQpxFn+hR/3N7FCT0iIfhFHxxdQKugjf7yc0tZ0NGVPIFciUjri9cYo3YoFwwLF2soJUL+MBW7DXI99zAqy8jPYV+nnoKdqhVMlspZ8HMIuAmzgo2FWRAqabfDIAEfBx3WCM2iJOGhTPdDxSO02b27MCwgN4epI8xvgWnD5dkahnLg/H3M9sTMuK+BRCVcBmBaRqM+Yg1ZINYImCVURCgD+bGDsuey4jbiNOHM2G15OfBzARsuH9jOPpFJR+6OAYFfLzntzxxJ8sn0J1EeybpfOJsnEyzhbSVmCN1OEDsJe0hfYX8JdI1uIXxjYx2Y681IT6PbyENyFeqe10lpi/x/lscnks478vEg6YEq/95VSJS7s0zE2IVAj7dSpnztJNV0tXPgYXpJOS2TexyjPBwb93JwoU88t4cfCjNA0kX0G8nIwH6gTO5AgkF+SBxC6P3Uz5N8wl8BVe7W5QPkfZQtquj23Ckmr73bi4qVsRtV2LgmY9DTM1n8+LN2L8KAV9sJVzqOXMjzquTj4I8rPzV1jkXeXXD9nPIT2busXQXEWeQNtNVUqmrnJWMaToz3EQ6k/77SN9FXE+5Fm+ifT+nfBKLVs1VHye/nfLdpAEpoK8kgbiNxTdaMeVSkuGl+lGPF0jHr1LAZCU0bJvjOFUZsq1ELEiA4ErrmAd59VZ2/CK+hTiP2EG0h87vA1DXAFLVZ6hp6c/EmaQL6Xex693EX+OTVset9K/Co4nTCBRAfV2P/THzt1gx+2+mQ1nhwoSCvMMqaAkrIbF9nvlUBfYetcKBhLQOp9/7+sSIvWcx/kG2/zDlLHowTKr6HpCqBqrPVJ+lHaQduAA/T7yS9FqWP4LdDk+H1+E4PJu4PzH0Cg4RlP3EbzP3Cqti6WYMRL4ykJAsn4nPrkLARSthM1sKTVRTboMYKEon9lhHnO9dW0jfhB+nfyxGFBh4omW41G7g84QwLKb6n/dTxBOZ/1PiDVY+a/w1i7dQXkicjy1EJYegfIzyRtJfs3yHVbG4l3mE4c3ttfjNCVZBS2MlNOxomB+uPEEoSAKhiH3WCed673l4GuPvxQnDZ6IEatJQZKtamRZw4CVlOdT4fMoL8D3EL+KjVkT7Zrov0T2O8hjiQspxpH3EV/BRvIM9l2LRqsn7h1ZEDz8P2rpKAXsrYYGtPaNAUSf+RZKEkIWEUIxYtA44zdvuw/yLiSdStgy/Sciokw1VRKsFrZ/7Sh3l6ux4oGgcW/Ew0h9R/pw7/gnLDs/luJodb2O8g36ebky5kxNuxz6Oc9f43BJ9ITKl2hNiYP/IeH4m2zLn2DoWIwghEDAZhYl4QhECSRxVAU/23/PEj9I8j+6kQz+k90iADlDLFnW0q8WqBE7V/TTV90iGSQ/Dg9j5ABb/DF9yeA6yfC2upUUL2OfusXSAdomyiVRHv4FIaPNM3gXPMUfJvZBMu19AL1Bk01OwJUeJk3xsK/13kX6H2AnUf4BQhkRAUlELWY3LgLihFnGYgHnS02jPIn4GtzgqfHSRh+8nNlWSGX43bG4mWXCxON9KuQGhTPUhZFAUoUGIEuKoCHgP795G87P0v0jsJAB1/m4Vz3x1qyVMA2MwICTDlAbfRtxG8zu4yhHnx3ou2Uscf4joNzSen8kzYKOMktQEQkgC5CkZCWXSB306CgJu8x9zLHwt/a9jO/3AEWSqWt40xbBYeXjZxWDtMCE5PAG1sN9Hl7jjN3CzI07spx8Qrr6H1QtYrISRMhfkiWAaTK6r/y0ClOVes98RJ/8Y49+nbAf6SQMoA283IEE9bRbSftxOvpP+StIdxD7yMrnFZspO0imkE0nbsRNzwyWbmgQOURbagh9nxyLxu7jeEWXXvrrkQgcDUs5IQEqbRGoUYCrpgDKRL5mMlMVWHHAEGXn3ReT/g7J9+Cd2qGqpyoZjiXQV5VLyJ3AJzRWcdDk6NW5pWLwnWy4gX0g8jHQf3B9bKMgwLKQ0VBpK+Bnsxm86opRu+NwYtXwo7YwEXG5JmQAUWSCEkGEiYRGwFEo5cvL99zmMns/4ZMrA0nGqzw6VTCzRf47mX0kfJ11G3Aws41oD9LiZ/Tfjv8nb2Xwy6VGUb8YT6LeTVjr9Vr2W8jTKu/BeR4wY061gCi6AaGeShNC15HyoZ6hUnbieBCwnpXdE+OI8fob+CRQUAHQoVXSpM9UMcID8+/SvYv+VPG0Z4a6xB3t4xZWc+F7KE/DTpItxuGRkoNRfthG/Oim5XOmI0HcUGBSvOnW1nUkSQpln3BAw8MK7TAlZlmh6M+e/R7TfTfMz9Fsoh4l41NMhcYD4N+LFuAxo8CfuPt2Y67+Cr+AvOen5xPeTziHl4cjHwL6Pp5Key95n4Q4zZ7w8sD1zoKRVmllNwQ0lkVBgOB0H/ZiDYeake1N+kNhen940vGgzoSDgKtp/Yv5Pcb2ZU36PuJz8y7iQaOsSDQWgvkY0eCJbH8kV70AxWwqljnrDPyCkGW1KGjeUVE+/hkschQgzp5s8X6kLowNLZ/O0iLfgl1l8G/Y7Itx+kP3/xoVfon8uvpEAVT+QFRc4k/gxzv4MrjNTLi8YXo5FNe5XGwG7VQgYiYCBkGw6AsXsBdw1R/l5+oYyvDFHqqMh3E7zq+x+PTpHlsIlH+fcp9OeiwtI9azCcFYMTyEeidcc6Qg4fNI+RJ5RErKcKYkY2nlVR6CgMzueknjRc2hOreUjBgrK0CHfSv+DvOs/0DlqvPtKfvTbGP0B/VORMTSj1AJsJ36bfW/FQTOjKwTDEa/+wUkzEnCpQaoLuZSBelBfEGbG755FfCNjFBiIFnVxOd1G/Cm3vIeHdI46n/4iD34p6QLSGQQMJFF1K/dm68PwHjMjKgHL4X5A0qySkEwkykAELNW9PujNjvSNjM+hh+FFAVQSegvNn3PPg9YNu9/H9j8g/QZlp2kUGK7DGf8wt70PxUzoAVBqCatmlknIUgJKbT76+g8HHZowEy7ZTHk45QTyQCG3ALIpxjQvoLvBuuL2JY77B7rH4RsBiBVUHOLBnHghPmsm3BlDq6AH9iWnGT8DFtONhA4JBWOAoJgN+x9CfhTy8JQbaNAjkArNCyhXWJd84WZOewZzT8BWyko3nd+Lpe9i9yUo1pjhVTAG6oBhRoXoA4mMfmhXPAJ9Jeda8+9znH0/ysmESRt4n9sBMspn8Vbrmmuu4Nx3EN+BoWhT/6VvpVzAju2405pzaxDqBkP3ZzUFO3QZpp/qTfXdjJ4Bd96T5YuJheHlVPXSqTQm/QP7v2Td0/8Z6bHEiYYjTl0YPoflc/Apa04fAMRK2sySkFS9dhvYoNyhwIyqA/M7WXooCeozuSohCxLSdbQfZmGfdU/+MuP3kL7zEAIOTXsnUE42E8LKBYQwqzIMukSYbnSgfg1GxGySkIMnEvcjIWD4iEyBHDSfYvFSxwQ7r+Pmd9B/AzYNr+yJ6Xunku7H1v/CkjUnqjZc9pphErI/kwY2KB9qDL0ZCHg67SYSDCyNzwBIeyZns+x2TPDfPff5Iul6nFdFuuEvmOZc9m1dewH7ICGqZrCf1btgBMpKzolDMRvKvRgPlJxyJWNG3IJPOaboryZfSTkPKCvIPMvkaBG3WXOGi8+kIzUFj4cPpdHX0/AMBezOJw1MtwW5zoj3E19yTLH3FrZeg57SAFG3eja6B2nBmhPD8lHdM8sIeBDQVxKODe8ZXbb2jE8CciVcroTMKChX849fdGyxyO9dTTpAbKMADEU/lJOIzdacWjrDEW+2Ai7XG1PqcsykQcywDthvJpChPnUAqnHcyvcWxxyxm1geWARQtQI7sXl2CYjDyzf7OqCq3jf49U3oka09461AGmj1ocH2OybpD5K64XfB6vEm4jgzo5buiEfA8QoOKIR+xlNwN0+CAfmqEwr1Y8ckpUMhBt4Fq68TJc8uATG8n/rITcGDGXA1Lkhmw7ibFpAGkAfOaGkbxyQl0+eBetxQRIwjF/nqa7POghcBECs4rgvC2tPtJ9VvO5CR62VYGC84JukXiDy8UUm9FnOZvN9MGH7mIw2M1/iIXvqBBah9/dM44yjY76egqZKNfkrABmFyfQ/enhCOKR60jTQi1G0gIdlN7JmtgBjeT400yym4q4QrlZhRj1GsOW23Vx8IZCKTAlCIBhAAp/KtJ+BmxxTlnqQFihV+M9LtdEuOCInhkx1mJeBiJVUtYy1dmY2Ao/GXzSNlSiIyMZWEJERBpmRKd4L2zgfgPx0rLF54AuVCYiKgAQGhB5SbWT5ozekHdhemgag3uwg4fCzD8DQ8AwG7L2shI5mIRkqUIAXREJPr5XI8zQXHlIDyKfQnDhR/h1ZHX2smZ/GU4chGJeZsyzCBOuoNL8tXMLbmbO6uJC1LaY6Ehkjkhr4QGcVESprmOBEXm9u0Bfutd256V+Ih96GcP7wxCaJuX2LuTmvO/kQc5uR/dzkitmQrowsi6BFqGetFqRo0ZhEBbyHdSDqTZBpNAgSBSEhZOFcsnonLrHuesYNyMf1WwIB0pvuD+Ap/st+a8/0D34liqI8ZZcHjoFTSqYvSpDE5MZKUkqw180s30rwP3y9lSiElIkiJUtCgEJM+8oM1HuaM0ZfQWc/c0N+b8h1IdfQzPCVfzdKX+KGwNqzyKyoS0qyTkPHA9FtIQS4o5J45jLokj609N96iPfHT5O+lyVKQEwUp0QalEBmBhrCFpW9xg3/H9dY1/ROJc4cTDyj1ZzfS3GomjBPJMLneCGYm3xPCUmBqCu6RyD1NT4vU0wajjjZIyZqz7+Qlxy9/Rso3iO40elKLnmiJQpPRolACmWieKo2/Fn9lvXLZA+5B9wtAgMMnIg7gM1y1y0w4KQ1Mt1WDPMskpA+KSSMVYNTT9pPIF5PrMcnsjgdM469o5i8jnSaj9GiJ+nSpTEHKsFnE09a1gOLHKaetUsBbKO/m9M5MWK7lg+Hvx5djRknIUlBC0wFtz6jQFEY9uaMtNGNSIfdJU5JZcMAV7tF/UEoPp91GQ5kjjYlR/SaEaIkg8gMVzzfuX4x91gufvzTxI99I/Bx9Ld/hZPwMt7/dzDgOUYtXSQcNEtLMBOylCKMeE/lGhXZMM6YJUtD05CAjzI7UvUPje6Vum2hpOmJERilEQUMKCkog0zQ/aS5/3scueRXCuuCHziY9i/5sYhWLAaC8ix1LZkdCteLc8HQsm42A+WAY9cwXck9TaCdRL8ekIaFFIwuzY59PON47iXsxJsboUWhaYo4oRJCD6NEiThbNr/tfD74CH3O0+eD9zyJ+lfhfh5evjopuorzKTGkzkAcEy9PRb4ZT8Gipt2kcmn6SaAQ5SMhTrTG5lnSSWRGW8UqNXwABY1KPEQXRokz6BkFEEum+YvwrlvMv4wpHl5+l/36MMCTc0PU/4RYzJdKhI15GnhZvxgJu39/LitZUpKt+AFokJJCMzJY7fM5J/lryYwAULKEnMjYREwlToSD6OdF+u1Fzb73v8P7Lv4ziiPINx7PwHMa/Mlx3g6KGQPo45S/NnNyQh954VBI2UGaTBc/ppSnhagnrSJglRTJrWn8peRzOAwUJOqDAGCNiRELKRFD6B8ijV3rivX5bGb8Pi2bNu+854qwziF+ifPfq1tIFIPaT/4buajNnLqOWDhmpatD0s6kDNopGqabaQ7eERntEBEw+qfEKyQuwAMIUy0SHnuiJQEtpaDIRj1Gav9DO/4kyfiV2myXbT/9O4+6nLHmEkuZXt5wpAN5LeS3pgJmTUi0goBYSzQyn4EYnC83AlNtM9RnJSJLNmjstOdWb9b4WT4BaQgplmdRROmKOPE80RMqaOEuJZ2rai/XxvJm8M/7cOSfSfr+2e7rkTFkyTiyvZiVxIF2DV/Lmmx0RvndUvemokpGmFrDMJgJmi0Z6DXIlW1MnRMgWJK0jwa0ut9MLzNumuJh61TqaIHpyj2WMKS0WJiK2p4j2u6TR10heL+JfLHZf4Oxd6KyWd787Of2RO21fuJel9I02lR9nfLJRok+M0SCjS/SIertpjTH5hXgDX+fIECPS0HTLtBCgHa9SwNaKWNBRTcGVfJWAc8iOFLu924leqvEPQguo65EUKMQyuSeCmEPQF3K/XTQ/rG+fYvPcx6Qb3qRvvqhbvMa5O2/A2BCv/ETyqEedYpxOcNGj7qNrL9b7OnNxgVIokMiJhAajxBiLiR59N7zOTnor7/hrR5TvGZEcfitsRoPoZrMpKes0QlMnHBwyOUkWJAuOJP/mVX7Ak/BjQh4+YQx6So+eWCbmaEaUEdHQ9KeK5puV0ddq+2u07TV27b9aP7pSTnt16aDIYyU1+rSgNNs86mHH099XTidr8rlSt02TspLoEwWRgJQm8iElErpgKdMHoea/6H+HxxVHlNSQTFE9c6kEaLqZRMCJgH2VaNQCkqYiYNjsSPJUxdjvWdAKP8Jhvv8lIfVTb046SiLK5F5DLnMin68058vRyQ6KtCw3HRFI5EZjTmdOlAUyqRAT8QIyGWlyDyJNP0MzTuTCGEuIBPBF2hey+9OOPJtMU0/Bcp2QjGdUiLYsK3XCUUsoIaExwiZHmn2+YovfAHynbIsY2F8NPXKgI3piTLSUeZNnQ7T0LaFV2m1kcksgZ5O9JzQNXSISgT6TEyUBMn2iR5PoJn2LLjEXLCVGGE3uHfTf+DW6j7DJkSe2oYo4qml30oyQlmazILWxLCkydSJCPR0jazDvaHCL693T8805iJ9B0iCGv4wIlKD06Mn9RMRJ/VBHNKRAJhUik1BaSiYK8kRApMl1n0goicgkpEwO+kyXaJAQiRbAZyx4Pj7iaLGYN5OQB9YApvovfnE2WXA7EXB6uq2lqxsLjha7XeUeftOcLfgezLGS3Y2AMomIZZItx4hoiYls0VAaSqb0yJOIlymJkkmJSKRJK4k86ROTMSmRUSbXPaT/Iv2+kfc4Wtx+1ibSdhIMFYCrZ8K8PKs64KKkG5Stvt+AOUeT3W53gt/U+KLsGcLxSv1dNkNf+ghB6smFGBMN/Yg8R8nkZhIh00S4TExE7Cdy9ZmUJtdpWk76oM8wERTyIt4jNb/qlP5SR5Pb2wUsEEgDG9PriDR30CpoV+xI2K2xrKmKz81gHRAWHG1udy1+2+k+KjxbeDzmFUAMtAYFEEEEpdCMKYvE3ETIhhhREhq6TGpIk3GbpoWkoCRSppnKjrtUhF2a/Dxf/NzfobjKUeZ+C2hJpptagukoqCzO5pT8ZI9kXEW6OvFQfb7VeuFG/+4UXxaeK3mi7EzB8D7vKQlLvfemYJFIpBHRURoiEy0pkZvJvYSJkGUiXMoIIgHZPpp369K/GvX/5KL7Wxd8cmkL80N7QAZqgXOLs4mA2V6NcZ35Dj8SgO3WEze50vF+zoKnCD8gfJ2wRUGqIl8ACsPfTBHkZcoypaEk8iQalnZyryHlqc8nEuZMZLJ36/MbpMVXO+uam6wnPnnxNkotHKA5ZMQhbptNIXqrg3qdNCBfQqbKkndYb9zpoLO8w6JLJG+U/YrGA8SAdAlNdQhXqc/hRN+TEP1Eskxu6FpSQz9PDpMISO96pXmNfv5PjZevwUG7Tre+KDum/2JRC3eotyE3zCYLXnankT1VtKt+EKraIPe3HtllGVfgCsd5gzmPl30bHoLzhG1CEuiRBs7krCNnTO3fz4XSkZcoLXm8JNxA8xmpfZPF/o24Td7LZuuU/hRaRCVa/TzYTo3jptlEwHe9bL9vfNZV1TvfYfkykrOsd/baj7c6xTv17oNvFx4iOUdxkuQkIUnIdeRDqqboyViPZK/karpb6D6jaz5g1L8D+8w7FrjvlFiH2QvSIHeUO2eThEDj2mqarWWsxTzZvHvgduud3Tpc6mU+7/l24ky9E4w8WHGG5FzFCXonY4dksyTphGJZuBO3GbtZcp3kK8a+LFwh3OgG19F3jinSvYk6+YCB/cB5F6ffsUoBz7Rikptq+WoB6xUxevfFBx0rPF3g9kmj8X47bNPZppg3sklnq2y70KBoHTC2W7Zo3kFL9tlvr/McRAH3dmzxvG88HtVDaRp46A9kpGu4oV+lgDdYMdlH6ihXPwNq6+K4x+GDjl0O2uMgbgZLaihoALAwaXscw8xfhFPISAPymRKgRXMZzCYJgd6VRjpZO7Ai5lDtYbZYwKINjiGai4idBAKQDvPdfPFZmE0SAm/4ozt9/y/eIDtjuPxSPx+6t0X3w6dscGzwe1+/ExeTjqsEA0BSRcclRl9avYBGVkX2CdkZlWhDWTDZ8bJHHksCbrD1NJxP5HqaIwHUUfFa4tbZRkDI3i77Jo2sGYx6JJNrJ8qegtfgZhusb5730BHjR9Hed7qmRD7MUvzyPtrrYTYrogFan5bdJDtl+L1wHR09WHL/Y0HADU45C1+H+eFDMVPdOvLHOXjn6gV00KrYZJex92t8Zx35BiNidqbk62Wfwm4brGPKwylPBNJKN8tfQ3wOS6sXcLXc5FYneLfsWzXaQ0XBQ2TGWfYdslfhYzZYx6SfIW8dPp63PhEVfIX5y6weLfNWxatfsexpP/1pXC67UFtJV19ntEjOxkslX42DNlhfPGs+85gfJx5Nj4wGgQDkQ61Nu4PmnSzdavVoWbJqGpcLl0kuQJKQVrBTrvEY2dfhtTZYZzzqYtqfhuHzanq09TfVX4P/Apj9FAwv/8PbPetpb9F6rMZJh5x6m4FpufG7sr14lw3WBz/9xPNIz6VcMPxVEACpkjN/gNs/dzcEvN1dInmb7CekWsDhpkFyjuTXzbkNn7DB0eVHnngy3S/QPJk8kPnWfQAKuj9noXMXySy4S+2lv3er7PclRR54H9xUPTRajccp/kj4GsFGO0rtFy48yejAHxDPIKbkA+gBkOpr5DfQf47eXW0tvbvMnLcrLpE9UKIWsX4WZPozj5S9VNiFSxxZNnjOheebm/t5Ed+lFPo8fCi6glLddz3jV7ibpPA8d4vnPe+7ZK/QOE5T1wLRHLZE8ynJy23yBuy1wWx5wUUjtx+8WJd/RTf3eEujHbqGgyO6FqNJm0Mz6UeYRzvpRz2jF9G+DLfC7JOQYd6t8Q+yXxwWbaAlZA/UeKGxBwkvwY02mA0vPjvZv+cnjfLPyc19pGh1iMQIfRCBqNaYRdV8gfKvPO+2NYiAa8Dvuw/+QOPJGo1m4OiQKjs+xPgmxcvxDsnVvsVuhA3uOg9Ic+6xcLKDm59kvPB9utGT9S3jecYtXctiO+lHLI3QYH6qn5uKjPNX0D4Db7YGpPAca8KLXvKtsj80cnq9YHWF2fH0GTSflbxR8kHh8/a7Bb0NVs4rHC8508HRRcrCN+vnHqOb22E8oowYz9G1jBu6EYtzLDfsn0eekm5uqrUYvRDPNcysC9EDHPBW222S/ZNUZ8ArOFMQMrI5ycWSB0luknzeTp+aLAO7zC0uq2XcAL8qe7CTLblIcn+Ni3XO1XT3Y3mzQGppglToe3LDKJGCvkdmuafLRKm+kLxD8zqal1lDUniGNeMV/3uzsb/S+J7B9YFtdZ0MrvqetJDtx63Yq3EZdglflFxv5EadfTbZZ9GiHQ643RI6a0UjORz7HI7hx/Ul7AcsT9qWSX8AT5C8yYJiXmezYrODtuM4vVONnas4z9g5xk5V3FNvm142Rsl0mxlvopunTKbcbp5uxHgSCccjDoxYHtGNMJqOfJ+geQbev8YC/tgah/6/Pl7xbK2f1th5yKRkZHJ9uL66hjS8OUvSC8uS/diL/RN5x4pO0gnLsl4RKCiSgkAIAQJQBIACQqkqFNBD9U8rU+OCTlIkk15IOo3epNfqjPSTVsxZtl1nm85WvTmdkQ5jdOgnrTC5T48OBcuIxHiefp7lzZSWfo7xHOMR/YguT8YtB0YszhMjjJZp3kf78/iCNaZl2Zry0z9wq7/+xz/Vu4fkB2WbpgXSACDVAtUSDok2eL+RbJJsEo6XAJBhYBxqSIhqL3Cu+vqfU6rPUr1/p+pLFfXrxLPUfxZT9+oEtT65ISOQEEHuJ30hCqmQe9pMZHKiKfQYoevpEpHfTXoBy18wA5rnuS/6tW1vvGCPb7r001rLssfK1eaple0nGfgqsvrXDoibhzbyV2OHuVb9Mw0cEqAaD/0AlcEvHq/GU00luWm5phoUpKHPE9FSEjEiT+RLiciYjFOmz6T0AX1+Fv3H6M2itSybCT/5Lbu84g1/YKvjZD8jaWVAMtys4p76eqoF6s+h/jxXf2n1rxtaDFzLkRGVeHVUSmiq6FpgIPJNmrb69dDXP5T1hrXqB7gUdKSgDXT0GUhIQQ6aQilhLv7DUnqhhfGnzZAUC19vprzorfNO8lTZL8keKlsYPl1/OIoMnRA7LOXgtDp8LwAO9y35A/cC9cFFZar11XWPbqovU/0YPZaZ3J+Mq8/HKFhGX///qnvjhrIwef5bYJKE6OfoJ4nHuPmCfuFvHPAK3GnGpDju8Y4If/nui7V+UfLtki1TEq5MwPoahjdqrZZhAROiaqpxLWUcRryYEqNDUcs4fD1GX40nvVJ93mM89Vk/STTKiH7LRL55xv9Pv2zcXGLc/J5rPvo69MyeFI4gr3NPje/S+DHZQySN5lDPcQNjh5u6hyQcmlprKukMSjcsYR0J+1UK2A3IOK7v121KuB4F40rAwPKIfoGyQDeiW6Af7TKe/1vj5m34kCNIivMf4ojyG59acLIHCN8t+wHZSZoBqepkhGEpwYCIUY9XEQUZFhAKYiXT8ICM40rIMeopNyZ9merHA5Gy/mcsI6Y/z5RN9JvoR2F54dW60d8po/e69NL9iCMroKPI22y3yc/qfbXs3rKTZK00FAWrxnCvygZV1xCwwnEMjAtU4qkiYKnFq6fdKmr1dUQbeCbsUI/rX9erx7fq5m5W5t9mPP+v+ISjSIqHH++o8uFbk/9yJp6o8RjJIzROk2yXhhOSgRpfLd4AqxDycBGxwGGm4BiKgJU0dTG5FqnDUvVrp5KPQXE7S4qr9a6w5D/wHjf7LJYdZVLc3/rheTY51X1wgeSrJs+JD5bskKRVZb9phVmwVdwrqBMTA5GvAOpMuKvGQ+L0hrPeOtKN1fKGzgG9qy15r3ApPmXRNbjWOiLFU60/fk2rsdOc4ySn4GxcqHFv2b1wFrbIsAIJoxJwSMSEAitIRADq+329kLiOgPUUXD+jUQs5uQ7Tny+bFrbTu87Y1RZ9QXKpzuUW3WLO9Tp3YNk6JMV3Ora4QPY9ttjteK1tGmfKTsAWybx+0mfzipF20tNKWiEjS7IyuSbJCAmUybhM3WO4LggqAXskoUcPQqATOhShCJ0i9DpFr1jS6U2asbFFnWWdJWOLegd1Dhq7TXGj3k16d9hlN3orZ0PADTbI/v/KBhsCbrDBhoAbbAi4wQYbAm6wIeAGG2wIuMGGgBtssCHgBhsCbrDB/wlKajuIhIz6AQAAAABJRU5ErkJggg==" /><span style="position:absolute;bottom:32px;left:50%;margin:-36px 0 0 -36px;"><img width="72px" height="37px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAABKCAYAAABU493xAAAS4ElEQVR4Ae3bV39Vd9739/f3vwXGNjOW7yu9eXOl12HSj2Jxn6THdp6ARXo3Tu+I9A6kHiKeQAand+Qc5SiD07s36XUsrmJjo72+WSl6BbCQZV9ze4bM/qzXe/f9Xwv0U9eOl7i//G/5Rz6QvEt3oM3hutNV3LPpe2nrL/6b/2EvY1uLcVN7jTou6fZItn1vbRpjxMvm3Jh2tNdAp9uxvjTL7NLrjz+/N7Px/diaT7xsfXHhtXcBBzKu1XFW8302fX+N+T/cy4b8CKof12b7RW6jXr7tl6dNW16SXnv8xfbjC69sQw2QeiOZlp5tBXDh8Zf/93Pm80PM+K0Lr+zEYpn0EPdO3NcXX2z/9vGnSdR6dfHxlw9w6AXN+1nC0/v67XPnLlvMkPRwXvfgpDWOj+l4XzjwkpS/5G/9B70MxWJ3cMc3tLa+ggMYxl7kOg5ifbW27tAdxxFPFevlCY85TpNbr33x2zdweMLXZZ9iOXF1YX1w4jpxWL2BW9Dk8phy84T9rZp+iHt+yRsjw8sgUw+xwgxoHWL1nBOrxX268/Rznh+eqVs/pTugeYD9xj2sIO21eVB+im0vKPLW8b6O1yAHoLbT3Jw67YxOy6xzn+6Qg2cexzJyZ3Razvwy2xqdvBQN9zCjtu7THentsuephuGEdkCn2699+XgPh55tex6M+0m36Sqmq+IAUsftTl3cTCznx/4EV5xQ0mvaw5iuiAMct6zFfSwXFtfLMpkOY3qPZx83dfHTsN3mGma/vI02XjbfpeqNJtc+v/Dq4czT/t+vd5YQ0xUc+Hr7I+sPATtr69n/tzmutqeR99ZjcTDzlFWn3gbstLZfsK/VSG+DMd6f+WU2jOGl863rKuwFJxnNB4D9WqxmXuD404xF8+7MsWcGbfJg5mvGdA8gce9F+xrro3ugtl/94vPtmV9WYz7xsvn2ZUW8UHoZOvWjmdPQT6BjvD1z7LiJj2dOsrZYAayt785O3KbFOAR48vrFN2Z+WY35xMvm51mslwDTmA5nTtOpK0htzxz7VW04bpOsp8OZ0zzTps0A1WLluMW5yzOnycgSsLLJsAk5gNG+M3MavAM6fTJz7Fe1YZM4uguSd9eZdmZOkozrWELSWzPHflUbNqnFvnoAC4s7sdidOTb/Hmx72Lpe3QOdbrdZzRz7VW3LJpCs3zv+SfHgTpubSR+I7c9feX0Z3YbGPRnXPFfqV7JhE6jFKtZXqndbh0m3saMuJ91uHTb9kL5HPe9XtfzVf8c/ZtPX+6rT5cW03oZTflG76a+Y/5xjY+M72gzQxmaANjYDtLEZoI3NAG1sbAZoYzNAG5sB2tgM0MbGZoA2NgO0sRmgjc0AbWxsBmhjM0AbmwHa2AzQxsZmgDY2A7SxGaCNzQBt/OrKfOIs/XP//e0dIz9ylG0GR/nEV4sDHPq2HW7vmBY/ItvWHs0e4MBL16b0T/+Tndof899eN+XabFtTHTHFjA6m7HviBla+qd/e/sBkz5Q3TlhnpYs93PU76X/+vbetx03rMA1miqkf4Z7fYY//jz9geW6M6zDBtFCcd34PD/2Klfo7nNjuP7O0tfUTUy5rmMbTb+yZ6sjs+PKHuO2k/o9p27kf/ISx8+zQHJ+bDRo6Dny5voqV79Lh77d0ND59Zu0OmpUnj3+MQ75768PlnSa7hKYVk5EaV3Dge+yz8eu3Mo03iInZdPf7Pobx31t6nr9iHp7z4z69TGnRUhStJCiF4Ja6rjzjt88vXXj9p4YdbZnFc+shCNhxbuvfwdJ3rgRKgsLSeO3azHe1PvyD3pXuUo6LJHD0vRtT35HuVncTuzGWM9+n8fwNf9Bf8Tct1X26pBVEERREKBFP11w3jdlg5mfTtjHu4y1KBI/oDaa/0MgVna5hRWlR0kvOnb/Phe2ZbweB9mtDFB+YPt+e+S6SxU3HtaUgbf2iaoVq/SLa8nzpfc0SJFEkEQe4y7SSc9syvaPdFSQoQbNnAjdcfOWOqUsJBQ+cy3tYAUdwwPq237qwp65rS6LT0qI3cdW3rpVES0KLYHrDeO0D3PAtWz/6td2YlppKAj0+j6hfTEGJ1C+g4an+wL/qw+vS5QxQUkw3jPUV5472nT86cP6Ley58eZUnl8jK0wXJdR0/Ie8CJV15Mq54bDXzNVuP99SeiLYC2bXuzsyZAQGUogURumealjPfxphcJzIXiERpS/1iqiCoZqGd+T6N4wt/4F/xVy+N7oEgoHTPmGaTr7n41UpyxbACEZ2KiHdpBemnxrTjtS8PZ17odz26gQMBpIzFnZkzg0ArBXgkOJbFnZkzO/xd1+mSclxbVSp+caVpTUlYS2a+T1trAV5ZXLcuCVNJiU/EDaf1w0crv/HGFfFT7baMUISIYurfg4fOVK9K/hstUvqWqbvYd+aKBKXoXckHGiDdMY0dHPimDi8uxS6VplMatZ+M3YJQdYYeu7g9jN3wduVyMpZtMDrxMXnAdBsrJ3Tk4nUGsyMydLuaSKfK0HcqS7Y6mcIwjK4tPsE9L+hL53a2xtb705QdxrLS8kgWD9rexT6sjSVb7w/DkSkM+Y/92/6Ya3/u0rT4VMMUs+oILmHlLP1v2++Sf15HTKFhGjMf4V3fpv/9125p/rpZTIMpB7jiLH325lL7qWl46t9zRXPdNN42hY7oGdd89IM77dglKhirqdNV2br/1G3Wdeq38cNr7xxl3Kos27QZIYgea8igYw83PNfaK1MjFbropGFoQgfSSkgnCaPNXLOPq57ricWSc3cqOwwVjDbCOD6eTu3DSa5uWXQtB00wqjGGATtSTxXD/mw1cya/7+E96W1KQAXJOzpdnjmzhX2EEMQO4+2Zb4a2lLYCWPcGjYRAdnS8PfNCn/1gqXYDCEbtbUkDCKjTmrx2/WjkXllWiWipzgElAaR7E3dmnlZNi0JFCpFSIkJJQhAwfc0T5y8nW/9+2QGKEpQq0GZuOeR+5d1QRRuaQan3UUqQ1uTuzLeSvCvQ54w7M2e2/X88EAeUY+mVmW8EcSyO23JAZg1QTPszL7RwHdUWcNBMd0FbKtRpjQvXY7pumkQFCg6jn0QO2j5AtaKqBu/jpqcLAVpNZwntlNA5ZglQDfV8T5xbRn+i3gwgSYEpdNV2JYVqBTVdExKoYvxR1373tmGHEijJymI6mDmzn20ff6FZTxckl3Vcmzkz0z0pQNXbyjcCQgu0jlvkqkBJSZam7M18zW+9uiN9XxCh0u7OgEiq1cTJPba17NQ9SVDtLJ+FD4e+GdOPh6MrW45+XP11yaplNG2ayAeTaWdmMjnq0aUji0tr49cnuRRWVUnA4NqXvrr0ZR9f+qq99MR06UnX8/n6w5lji+QOlqJUgvbhMK6N9s2F6dI560u/1SdvVv8isiqEdkpbEGxZnN8xtZJoEfRj36b/4/UlvU4gtJJQqpKIv9vnX+zj0Fma3vxYABHtjmb7TM8Pijz/9j1a6dYeZlAyPpDFra+tO3LH1KASmezTh3CkCSB0arH2fBecv45WM2eSR5or+MRznfNk9aTTj3ntfjNdThdtmoWt6ziABSseO+5JX5GmTVPB9OiCxQp44qSOvLoz1Q6lCGpFrjCtPNU2h3y1j3tPvHoT7zdzBaqGuoygqJT4SDi78/elEQTxEV2hkgDedOG1mzNn8muHD3AoRVGSH82cCrQiCK2tI44dPb6NQwHotmn6YMaxx2MXS0EEHptuzMxsSQORSkR8rQtLsUtBiU43ePLJzEnOWR+y/jBNmylabS+ntmeeB4hC4wzV4oNEk5AmVb66wuPVzAscnvPFh20fhkoVMMSPBBoRcPTkwcyZ/OwH1+lSoCjT9KHkmghFCWKXaWfmbKwcl9BemjkVRE78OgguOpTe1pYSxDVffbWcMSPXKQpM3bvAambmSFMFWoNuSWeOYYcSiRaHiVszpzmfzw+aPkyRSGyv2Zl5HqAJsD4D8XbbqGpF7w6L1czpnhyW29ooUdSQvgUo4NC5xWrmGx1uL8ke9VQ3bFlZHH1EDwSBEnRxc+ZM0geCwGT21szpHgNaL+ro8S3xUAK0bzj3yvUZ47XrWAoSWOGuF5RQQkMdG8k7aRtaSUwfz5yF9mOBqgo/Cp4HTGkrqYXRmRfh3GXtmwENTPrRzFksjJ9WJQhJuoVLgggYXTlr58ZNUzFoSVaO7Dsu45p6ACgluayu4ZYzVRKEkaUz1RIU6vkuOvSFD+lPJETorvXigO5KqGpDb1hYeaqt9bmWThpVSY48W3S7EdBi/Bni06khw9SoIBgkKgjJNihJlKUTStC0RGGKU1p47Y0ppRVpNaxXztgWJtEitM2gbwClLTkkvtFnP3yfvgMUmKYbzq1XM2a2vvhEp1u0IpIZ5Lov18uZU8VD0FacvUAJRuKkXv3invaAAlSmfemSkkasLOzPPANtAUVt8Yy2SwUE6ZutZZIlliMzZnkr6ZIuw5JZuz1DtRNeoFVN4kw90UsKVBMpX61mzuLIOlKUFjUkkRJEmHyjRz9YSvcQgRIrW/ZnnjGObohHFAVi2ytbd2ZONU0VRChnLyJAkRJf094QKBrHBW112pv5Gt98TCMOg7SNgmi1TZUSpPFUqcYsAUlElvH1DRK0nOlTGAIpRfOKH74xcxYcgYRIgy2KAOpMjfV16/GWlCqhveqkLhwd+u0LH9I7QBGandkHuO1FDbRQSdQZa0EaTWmc1MXHB3779QOxoyqiUDI+tnDXCR09aRbSEpFo6/m6klxuG02Z/t1J7tBM1nSBCcN6xnHFoEdiYV0WfOaEqiJIVdamOKXhqwft+UoRpV/28Zt46EydpwXVVGxJV9olSGiWTuu3Ln6gdilCRLpv0QMv6oe/se/RD96X7khoCZo9T8ZHWDmpkcvWiGhLH/mmLuBJogBFvLCur7L4VBLtU4M63/6Ctqw7WSSiZsRzpVa0MlKSjj9kwV1qAY4cd85364nfj5Y0Gqy/4fHnVltoG1KaYbGDB75NiVSRoVao42JpOvrRzNc8emVJbwmiAlbavZlTbeUqPaTEsW1bvTNzovayACIyfTpzKtAKIOK0Lj5ekfckN8Tfw7QnvWphNXOip0qnOqG16SNJHJdeIm8TPzeJSLWCWLwx8yKvenJIPwmlSBvvzJxFbL2foCUiDMmBNIJAa2ztzTzjN19fOjfu03qm3rDow5lT/eBwhT2gBYq+TW9Sz/j81V2x1KkUGNPHM6eCCEgBR6e7eHjPxZ/t+eFs+2c3vPm/78+8yJFFQqlGhtZzbTk6oIcUBTXtP/ab2zPf5ImL7z/22nLmRdIeThpAw+XgNPQjRIQmtdO6NnOao77xfnVXC7SCYfgYBApR75rGHdPibV/Nvjx33Sv5qViKUCJM+8b/zZn82me3pQciWoKIKR+Y9+do7Hh8fsdXF64b65uUkQjSleSBs/8kmrZSv0dqzaq0Tq69pQRthbfOu3ifC8uZk21vj/H73Zxin8WdmReZ9JOESGmi70zWy5kXOXJ0S3ymQFXkJq55QZPXP6j1fpVEArST/Gf+dX/k3/Tn3tfsPP03NE+9XCemQWdr1REN01gZix089G36H95YGuO+KcunXtJTTUwDs3Voakpm1RFxFfu+qcMfLh2NTzUcH/O6vxsHfk4d/ffLnUXcJ3qsufL8Pn6L7Vdz4SetHUaJChmm9u5k8VE4hNh6q8PlaRq7jDdKZGhzCx86oSde2WXcIWRoRyceDjmYjIO1ZLA9cYh9gDr/QS1ulhDNoFFjRQ7WfLyQThaXsduM7TaeOv4SU5NBWBwdf31SQCkRxykBtCsdV6z7cOZb+f0OV3hPHFIgggo6EWgEQbqi+9Q3Q0BBElv8PG05QmnrlC5y2LqKlUg1VDWR90f8JMl9Gfeb7reuhTekgVTpu2xtzzzvnPU9spJoCxLLKXbDnaF3KjdjvD1zbDi6Te8GSRop0CXdXSR3yr70g+q2KkUPQJvOokbUf/EP/esrI9ekoahnSgGNMa0s1u8593g18538gf/jA+kVunJchEIpgcJDeoU6G2hFaAVH6czPDdI5AqFe0JbHK1xJe09ATvw5UlN1HGEy/ZN8/mMeHc6c4PCcxbtMh8AUraBpIOqkFr66Wr2taCMkgCqozKHBjS1jL6FASQdB/Bf/yL9813pxCQdGIhwDKXHP6BULD2Z+R/6QeYhy7ophX1qKPj9IB+oKVr5NSSgRsNXM/NxIG0laigZOG6KFL96bOl2VPBgUIihVaaD6SHur7RV6jVcOZ17ss0/WXfw4HAiRaqmCU4f7q2tr/YuqK60ZqAZC5w5qfYWjPUCDEJr85/51z/dHXHtnaeo7LN40jZry0PnFPRz6PdF/tlx64rLOpgErj88fYOX/hz3+9/6s5RNHl2PrsoEprTwcFg+eeLLC4Xda1++7HHp5srjM6OzhkWl1ZOvBN6/5+mXMspwwjBXjAWYv7v8Et4Nz+w5xmR4AAAAASUVORK5CYII=" /></span></div><script type="application/json" data-content-len="91" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","splash_screen_show"]]]}</script> +<div id="mount_0_0_aR"></div> +<link type="text/css" rel="stylesheet" href="https://static.cdninstagram.com/rsrc.php/v5/ya/l/0,cross/z8FXlCP5lIcKlxREwiTz-jcyf8QhNEWE-MbVu6J6TSgCAgbkPiBd1Gm.css" data-bootloader-hash="XGqFsxj" data-async-css="1" crossorigin="anonymous" data-p=":17,78,25,39,41" data-btmanifest="1021044255_main" data-c="1" /><script type="application/json" data-content-len="53" data-sjs>{"require":[["maybeDisableAnimations",null,null,[]]]}</script> +<script type="application/json" data-content-len="49" data-sjs>{"require":[["replaceNativeTimer",null,null,[]]]}</script> +<script type="application/json" data-content-len="60" data-sjs>{"require":[["bootstrapWebSession",null,null,[1742402983]]]}</script> +<script type="application/json" data-content-len="190" data-sjs>{"require":[["qplTagServerJS",null,null,[["not_accepts_html","aborting_preloading","ssr_disabled_reason:fail_ssr_disabled","comet_aa_coinflip:false","should_use_bt_safe_js","logged_out"]]]]}</script> +<script type="application/json" data-content-len="80" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierOne"]]]}</script> +<script type="application/json" data-content-len="478" data-sjs>{"require":[["CometSSRMergedContentInjector","ssrInit",null,[{"enabled":false,"cavalry_get_lid":"7483563829649516416","success_status":"success","disabled_status":"fail_ssr_disabled","bad_preloaders_status":"fail_bad_preloaders","eid":"mount_0_0_aR","should_ignore_static_id":true,"gks":{"comet_ssr_wait_for_dev":false,"mwp_ssr_enabled":true,"stop_render_at_splashscreen":false,"use_content_visibility_hidden":true,"comet_ssr_unhide_early":false},"is_in_crawler_mode":false}]]]}</script> +<script type="application/json" data-content-len="82" data-sjs>{"require":[["JSScheduler","makeSchedulerGlobalEntry",null,["null",false,false]]]}</script> +<script type="application/json" data-content-len="240699" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:310",["RunWWW"],{"__rc":["RunWWW",null]},-1],["cr:1078",[],{"__rc":[null,null]},-1],["cr:1080",["unexpectedUseInComet"],{"__rc":["unexpectedUseInComet",null]},-1],["cr:1126",["TimeSliceSham"],{"__rc":["TimeSliceSham",null]},-1],["cr:3725",["clearTimeoutWWWOrMobile"],{"__rc":["clearTimeoutWWWOrMobile",null]},-1],["cr:4344",["setTimeoutWWWOrMobile"],{"__rc":["setTimeoutWWWOrMobile",null]},-1],["cr:6640",[],{"__rc":[null,null]},-1],["cr:7385",["clearIntervalWWW"],{"__rc":["clearIntervalWWW",null]},-1],["cr:7386",["clearTimeoutWWW"],{"__rc":["clearTimeoutWWW",null]},-1],["cr:7388",["setIntervalWWW"],{"__rc":["setIntervalWWW",null]},-1],["cr:7389",["setIntervalAcrossTransitionsWWW"],{"__rc":["setIntervalAcrossTransitionsWWW",null]},-1],["cr:7390",["setTimeoutWWW"],{"__rc":["setTimeoutWWW",null]},-1],["cr:7391",["setTimeoutAcrossTransitionsWWW"],{"__rc":["setTimeoutAcrossTransitionsWWW",null]},-1],["cr:8959",["DTSG"],{"__rc":["DTSG",null]},-1],["cr:8960",["DTSG_ASYNC"],{"__rc":["DTSG_ASYNC",null]},-1],["cr:696703",["JSScheduler"],{"__rc":["JSScheduler",null]},-1],["cr:806696",["clearTimeoutComet"],{"__rc":["clearTimeoutComet",null]},-1],["cr:807042",["setTimeoutComet"],{"__rc":["setTimeoutComet",null]},-1],["cr:896461",["setIntervalComet"],{"__rc":["setIntervalComet",null]},-1],["cr:896462",["setIntervalComet"],{"__rc":["setIntervalComet",null]},-1],["cr:925100",["RunComet"],{"__rc":["RunComet",null]},-1],["cr:986633",["setTimeoutComet"],{"__rc":["setTimeoutComet",null]},-1],["cr:1003267",["clearIntervalComet"],{"__rc":["clearIntervalComet",null]},-1],["cr:70",["FBInteractionTracingDependencies"],{"__rc":["FBInteractionTracingDependencies",null]},-1],["cr:534",[],{"__rc":[null,null]},-1],["cr:619",["setTimeoutCometLoggingPriWWW"],{"__rc":["setTimeoutCometLoggingPriWWW",null]},-1],["cr:686",[],{"__rc":[null,null]},-1],["cr:734",[],{"__rc":[null,null]},-1],["cr:755",["warningWWW"],{"__rc":["warningWWW",null]},-1],["cr:851",[],{"__rc":[null,null]},-1],["cr:866",[],{"__rc":[null,null]},-1],["cr:873",["InteractionTracing"],{"__rc":["InteractionTracing",null]},-1],["cr:945",[],{"__rc":[null,null]},-1],["cr:984",[],{"__rc":[null,null]},-1],["cr:1033",["FalcoLoggerTransportsDeferred"],{"__rc":["FalcoLoggerTransportsDeferred",null]},-1],["cr:1293",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["cr:2099",[],{"__rc":[null,null]},-1],["cr:2448",["useHeroBootloadedComponent"],{"__rc":["useHeroBootloadedComponent",null]},-1],["cr:2602",[],{"__rc":[null,null]},-1],["cr:2654",[],{"__rc":[null,null]},-1],["cr:2701",[],{"__rc":[null,null]},-1],["cr:2928",["relay-runtime"],{"__rc":["relay-runtime",null]},-1],["cr:3404",["createRelayQPLLogger"],{"__rc":["createRelayQPLLogger",null]},-1],["cr:3976",[],{"__rc":[null,null]},-1],["cr:4001",[],{"__rc":[null,null]},-1],["cr:4345",[],{"__rc":[null,null]},-1],["cr:4874",[],{"__rc":[null,null]},-1],["cr:5473",[],{"__rc":[null,null]},-1],["cr:5655",[],{"__rc":[null,null]},-1],["cr:5906",[],{"__rc":[null,null]},-1],["cr:7063",["CometErrorBoundary.react"],{"__rc":["CometErrorBoundary.react",null]},-1],["cr:7162",["ReactDOMCompatibilityLayer"],{"__rc":["ReactDOMCompatibilityLayer",null]},-1],["cr:7269",["handleCometErrorCodeSideEffects"],{"__rc":["handleCometErrorCodeSideEffects",null]},-1],["cr:7299",[],{"__rc":[null,null]},-1],["cr:7329",[],{"__rc":[null,null]},-1],["cr:7383",["BanzaiWWW"],{"__rc":["BanzaiWWW",null]},-1],["cr:7422",["ImageDownloadTrackerWWW"],{"__rc":["ImageDownloadTrackerWWW",null]},-1],["cr:7438",[],{"__rc":[null,null]},-1],["cr:7451",[],{"__rc":[null,null]},-1],["cr:7581",["handleCometReauthenticationSideEffects"],{"__rc":["handleCometReauthenticationSideEffects",null]},-1],["cr:7730",["getFbtResult"],{"__rc":["getFbtResult",null]},-1],["cr:7941",[],{"__rc":[null,null]},-1],["cr:8546",["FDSCometTooltip.react"],{"__rc":["FDSCometTooltip.react",null]},-1],["cr:8907",["HeroTracingCoreConfigWWW"],{"__rc":["HeroTracingCoreConfigWWW",null]},-1],["cr:8908",["HeroTracingCoreDependenciesWWW"],{"__rc":["HeroTracingCoreDependenciesWWW",null]},-1],["cr:8958",["FBJSON"],{"__rc":["FBJSON",null]},-1],["cr:9984",[],{"__rc":[null,null]},-1],["cr:11054",[],{"__rc":[null,null]},-1],["cr:11192",[],{"__rc":[null,null]},-1],["cr:13142",[],{"__rc":[null,null]},-1],["cr:15957",[],{"__rc":[null,null]},-1],["cr:719780",[],{"__rc":[null,null]},-1],["cr:844180",["TimeSpentImmediateActiveSecondsLoggerComet"],{"__rc":["TimeSpentImmediateActiveSecondsLoggerComet",null]},-1],["cr:955714",[],{"__rc":[null,null]},-1],["cr:994756",["BaseCometModal.react"],{"__rc":["BaseCometModal.react",null]},-1],["cr:1064332",[],{"__rc":[null,null]},-1],["cr:1106516",[],{"__rc":[null,null]},-1],["cr:1108857",[],{"__rc":[null,null]},-1],["cr:1110430",[],{"__rc":[null,null]},-1],["cr:1121434",[],{"__rc":[null,null]},-1],["cr:1132918",[],{"__rc":[null,null]},-1],["cr:1267207",[],{"__rc":[null,null]},-1],["cr:1294158",["React.classic"],{"__rc":["React.classic",null]},-1],["cr:1294159",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["cr:1467370",["CometRelayScheduler"],{"__rc":["CometRelayScheduler",null]},-1],["cr:1522191",["CometLinkTrackingUtils.facebook"],{"__rc":["CometLinkTrackingUtils.facebook",null]},-1],["cr:1645510",[],{"__rc":[null,null]},-1],["cr:1752405",["QPLAddCometRequestHeaders"],{"__rc":["QPLAddCometRequestHeaders",null]},-1],["cr:1824473",[],{"__rc":[null,null]},-1],["cr:1984081",[],{"__rc":[null,null]},-1],["cr:662",["PolarisBrowserCookieConsentModal.react"],{"__rc":["PolarisBrowserCookieConsentModal.react",null]},-1],["cr:777",[],{"__rc":[null,null]},-1],["cr:1333",[],{"__rc":[null,null]},-1],["cr:2221",["PolarisMobileNav.react"],{"__rc":["PolarisMobileNav.react",null]},-1],["cr:2696",["PolarisDesktopNav.react"],{"__rc":["PolarisDesktopNav.react",null]},-1],["cr:2882",[],{"__rc":[null,null]},-1],["cr:3104",["PolarisPostRootLoggedOut.react"],{"__rc":["PolarisPostRootLoggedOut.react",null]},-1],["cr:4197",["PolarisLoggedOutCallToAction.react"],{"__rc":["PolarisLoggedOutCallToAction.react",null]},-1],["cr:4347",[],{"__rc":[null,null]},-1],["cr:5384",[],{"__rc":[null,null]},-1],["cr:5385",[],{"__rc":[null,null]},-1],["cr:5527",[],{"__rc":[null,null]},-1],["cr:5919",[],{"__rc":[null,null]},-1],["cr:5941",[],{"__rc":[null,null]},-1],["cr:6115",[],{"__rc":[null,null]},-1],["cr:6397",[],{"__rc":[null,null]},-1],["cr:7056",[],{"__rc":[null,null]},-1],["cr:7298",[],{"__rc":[null,null]},-1],["cr:7387",["requestIdleCallbackWWW"],{"__rc":["requestIdleCallbackWWW",null]},-1],["cr:7417",[],{"__rc":[null,null]},-1],["cr:7457",["PolarisMobileTopNavLoggedOut.react"],{"__rc":["PolarisMobileTopNavLoggedOut.react",null]},-1],["cr:10761",[],{"__rc":[null,null]},-1],["cr:13684",[],{"__rc":[null,null]},-1],["CometPersistQueryParams",[],{"relative":{},"domain":{"instagram.com":{}}},6231],["GetAsyncParamsExtraData",[],{"extra_data":{}},7511],["BootloaderConfig",[],{"deferBootloads":false,"enableLoadingUnavailableResources":false,"jsRetries":[200,500],"jsRetryAbortNum":2,"jsRetryAbortTime":5,"silentDups":true,"timeout":60000,"tieredLoadingFromTier":100,"hypStep4":true,"phdOn":false,"phdSeparateBitmaps":false,"btCutoffIndex":1477,"fastPathForAlreadyRequired":true,"earlyRequireLazy":false,"enableTimeoutLoggingForNonComet":false,"deferLongTailManifest":true,"lazySoT":false,"csrOn":true,"translationRetries":[200,500],"translationRetryAbortNum":3,"translationRetryAbortTime":50},329],["CSSLoaderConfig",[],{"timeout":5000},619],["CurrentUserInitialData",[],{"ACCOUNT_ID":"0","USER_ID":"0","NAME":"","SHORT_NAME":null,"IS_BUSINESS_PERSON_ACCOUNT":false,"HAS_SECONDARY_BUSINESS_PERSON":false,"IS_FACEBOOK_WORK_ACCOUNT":false,"IS_INSTAGRAM_BUSINESS_PERSON":false,"IS_MESSENGER_ONLY_USER":false,"IS_DEACTIVATED_ALLOWED_ON_MESSENGER":false,"IS_MESSENGER_CALL_GUEST_USER":false,"IS_WORK_MESSENGER_CALL_GUEST_USER":false,"IS_WORKROOMS_USER":false,"APP_ID":"936619743392459","IS_BUSINESS_DOMAIN":false,"NON_FACEBOOK_USER_ID":"0","IS_INSTAGRAM_USER":1,"IG_USER_EIMU":"0"},270],["DTSGInitialData",[],{},258],["LSD",[],{"token":"AVqzJ_kxCRU"},323],["ServerNonce",[],{"ServerNonce":"XubyT0omBNRw8YS_HiGPrE"},141],["SiteData",[],{"server_revision":1021044255,"client_revision":1021044255,"push_phase":"C3","pkg_cohort":"HYP:instagram_web_pkg","haste_session":"20166.HYP:instagram_web_pkg.2.1...0","pr":1,"manifest_base_uri":"https:\/\/static.cdninstagram.com","manifest_origin":"instagram","manifest_version_prefix":"","be_one_ahead":true,"is_rtl":false,"is_experimental_tier":false,"is_jit_warmed_up":true,"hsi":"7483563829649516416","semr_host_bucket":"","bl_hash_version":2,"comet_env":7,"wbloks_env":false,"ef_page":"PolarisPostRoute","compose_bootloads":false,"spin":4,"__spin_r":1021044255,"__spin_b":"trunk","__spin_t":1742402983,"vip":"157.240.3.174"},317],["SprinkleConfig",[],{"param_name":"jazoest","version":2,"should_randomize":false},2111],["PromiseUsePolyfillSetImmediateGK",[],{"www_always_use_polyfill_setimmediate":false},2190],["JSErrorLoggingConfig",[],{"appId":936619743392459,"extra":[],"reportInterval":50,"sampleWeight":null,"sampleWeightKey":"__jssesw","projectBlocklist":[]},2776],["ImmediateImplementationExperiments",[],{"prefer_message_channel":true},3419],["DTSGInitData",[],{"token":"","async_get_token":""},3515],["UriNeedRawQuerySVConfig",[],{"uris":["dms.netmng.com","doubleclick.net","r.msn.com","watchit.sky.com","graphite.instagram.com","www.kfc.co.th","learn.pantheon.io","www.landmarkshops.in","www.ncl.com","s0.wp.com","www.tatacliq.com","bs.serving-sys.com","kohls.com","lazada.co.th","xg4ken.com","technopark.ru","officedepot.com.mx","bestbuy.com.mx","booking.com","nibio.no","myworkdayjobs.com","united-united.com","gcc.gnu.org"]},3871],["InitialCookieConsent",[],{"deferCookies":false,"initialConsent":[1,2],"noCookies":false,"shouldShowCookieBanner":false,"shouldWaitForDeferredDatrCookie":true,"optedInIntegrations":["adobe_marketo_rest_api","chili_piper_api","giphy_media","google_ads_pixel_frame_legacy","google_ads_pixel_legacy","google_ads_remarketing_tag","google_ads_services","google_cached_img","google_double_click_uri_connect","google_fonts_font","google_paid_ads_frame","google_paid_ads_img","google_translate","google_universal_analytics_legacy","google_universal_analytics_legacy_img","google_universal_analytics_legacy_script","linkedin_insight","reachtheworld_s3","twitter_analytics_pixel","twitter_legacy_embed","youtube_embed","advertiser_hosted_pixel","airbus_sat","amazon_media","apps_for_office","arkose_captcha","aspnet_cdn","autodesk_fusion","bing_maps","bing_widget","boku_wallet","bootstrap","box","cardinal_centinel_api","chromecast_extensions","cloudflare_cdnjs","cloudflare_datatables","cloudflare_relay","conversions_api_gateway","demandbase_api","digitalglobe_maps_api","dlocal","dropbox","esri_sat","facebook_sdk","fastly_relay","gmg_pulse_embed_iframe","google_ads_conversions_tag","google_drive","google_fonts_legacy","google_hosted_libraries","google_oauth_api","google_oauth_api_v2","google_recaptcha","here_map_ext","hive_streaming_video","iproov","isptoolbox","jquery","js_delivr","kbank","mathjax","meta_pixel","metacdn","microsoft_excel","microsoft_office_addin","microsoft_onedrive","microsoft_speech","microsoft_teams","mmi_tiles","open_street_map","paypal_billing_agreement","paypal_oauth_api","payu","plaid","platformized_adyen_checkout","plotly","pydata","recruitics","rstudio","salesforce_lighting","stripe","team_center","tripshot","trustly_direct_debit_ach","twilio_voice","unifier","unsplash_api","unsplash_image_loading","vega","yoti_api","youtube_oembed_api","google_apis","google_apis_scripts","google_img","google_tag","google_uri_frame","google_uri_script"],"hasGranularThirdPartyCookieConsent":true,"exemptedIntegrations":["advertiser_hosted_pixel","airbus_sat","amazon_media","apps_for_office","arkose_captcha","aspnet_cdn","autodesk_fusion","bing_maps","bing_widget","boku_wallet","bootstrap","box","cardinal_centinel_api","chromecast_extensions","cloudflare_cdnjs","cloudflare_datatables","cloudflare_relay","conversions_api_gateway","demandbase_api","digitalglobe_maps_api","dlocal","dropbox","esri_sat","facebook_sdk","fastly_relay","gmg_pulse_embed_iframe","google_ads_conversions_tag","google_drive","google_fonts_legacy","google_hosted_libraries","google_oauth_api","google_oauth_api_v2","google_recaptcha","here_map_ext","hive_streaming_video","iproov","isptoolbox","jquery","js_delivr","kbank","mathjax","meta_pixel","metacdn","microsoft_excel","microsoft_office_addin","microsoft_onedrive","microsoft_speech","microsoft_teams","mmi_tiles","open_street_map","paypal_billing_agreement","paypal_oauth_api","payu","plaid","platformized_adyen_checkout","plotly","pydata","recruitics","rstudio","salesforce_lighting","stripe","team_center","tripshot","trustly_direct_debit_ach","twilio_voice","unifier","unsplash_api","unsplash_image_loading","vega","yoti_api","youtube_oembed_api"]},4328],["BootloaderEndpointConfig",[],{"debugNoBatching":false,"maxBatchSize":-1,"endpointURI":"https:\/\/www.instagram.com\/ajax\/bootloader-endpoint\/"},5094],["IntlCurrentLocale",[],{"code":"en_US"},5954],["TransportSelectingClientContextualConfig",[],{"rawConfig":"{\"name\":\"rti\/web_rs_transport_selecting_client\",\"cctype\":\"dense\",\"version\":1,\"policy_id\":\"static\",\"sample_rate\":1000,\"contexts\":[{\"name\":\"method\",\"type\":\"STRING\",\"callsite\":true,\"buckets\":[{\"name\":\"rollout_group_1\",\"strategy\":\"in\",\"values\":[\"FBGQLS:FEEDBACK_LIKE_SUBSCRIBE\",\"Falco\",\"FBLQ:comet_notifications_live_query_experimental\"]},{\"name\":\"rollout_group_6\",\"strategy\":\"in\",\"values\":[\"FBGQLS:COMMENT_CREATE_SUBSCRIBE\",\"FBGQLS:COMMENT_LIKE_SUBSCRIBE\",\"FBGQLS:FEEDBACK_COMMENT_PERMISSION_TOGGLE_SUBSCRIBE\",\"FBGQLS:FEEDBACK_TYPING_SUBSCRIBE\"]},{\"name\":\"rollout_group_4\",\"strategy\":\"regex\",\"values\":[\"FBGQLS:.*\"]},{\"name\":\"rollout_group_3\",\"strategy\":\"regex\",\"values\":[\"FBLQ:.*\"]},{\"name\":\"skywalker\",\"strategy\":\"in\",\"values\":[\"SKY:test_topic\",\"live\/api\/copyright\",\"intern_notify\",\"locplat\/ttm\",\"rti_widget_dashboard\",\"srt\/user_metrics_counter\",\"media_manager_instagram_composer_create_update\",\"cubism_annotations\/fleet_health\",\"srt\/notifications\",\"ads\/reporting\/snapshot\",\"unidash\/widget\",\"cubism_annotations\",\"ads\/reporting\/export\",\"pubx\/notification\/update\",\"ads\/powereditor\/import\",\"lwi_async_create\",\"video_edit\",\"metric_graph_realtime\",\"vcc_video_posting_www\",\"cms\/object_archive_copy_created\",\"cms\/branch_updated\",\"cms\/object_saved\",\"codeless_event_tracking\",\"srt\/job_updated\",\"video_broadcast\",\"video\/broadcast\/error\",\"vcpanel\/api\",\"lwi_everywhere_plugin\",\"commercial_break_v2\",\"advanced_analytics\/query\",\"cubism_annotations\/ads_mastercook_models\",\"gqls\/comment_like_subscribe\",\"live\/api\/copyright\",\"shiba\/mock_bot_error\",\"shiba\/save_state\",\"video_list_publishing_progress_update\",\"assistant_wizard\",\"gizmo\/manage\",\"collab\/presentation\/request\",\"snaptu\/push_notif\"]},{\"name\":\"skywalker_bulletin\",\"strategy\":\"in\",\"values\":[\"www\/sr\/hot_reload\"]},{\"name\":\"rollout_group_5\",\"strategy\":\"regex\",\"values\":[\"Collabri|RealtimeClientSync:.*\"]},{\"name\":\"default\",\"strategy\":\"catch_all\"}]}],\"outputs\":[{\"name\":\"group\",\"type\":\"STRING\"},{\"name\":\"dgwUpsampleMultiplier\",\"type\":\"FLOAT\"}],\"vector\":[\"group1\",\"0.01\",\"group6\",\"0.001\",\"group4\",\"1.0\",\"group3\",\"1.0\",\"skywalker\",\"1.0\",\"skywalker_bulletin\",\"1.0\",\"group5\",\"1.0\",\"default_group\",\"1.0\"],\"vectorDefaults\":[\"default_group\",\"1.0\"],\"timestamp\":1663366072}"},5968],["CookiePrivacySandboxConfig",[],{"is_affected_by_samesite_lax":false},7723],["CookieDomain",[],{"domain":"instagram.com"},6421],["IntlVariationHoldout",[],{"disable_variation":false},6533],["RtiWebRequestStreamClient",[],{"ThrottledMethods":{},"overrideHeaders":{}},6639],["IntlNumberTypeProps",["IntlCLDRNumberType05"],{"module":{"__m":"IntlCLDRNumberType05"}},7027],["JSSelfProfilerTrackedInteractions",[],{"interactions":[{"action":"*","tracePolicy":"*"}]},6918],["ClickIDURLBlocklistSVConfig",[],{"block_list_url":["https:\/\/www.youtube.com\/watch?v=f1J38FlDKxo","https:\/\/www.youtube.com\/watch?v=6xt7nTuO85A"]},7631],["CookieCoreConfig",[],{"_fbp":{"t":7776000},"csrftoken":{"t":31449600},"dpr":{"t":604800},"ds_user_id":{"t":7776000},"ig_lang":{"t":34560000},"igd_ls":{"t":34560000},"locale":{"t":604800},"wd":{"t":604800}},2104],["IntlPhonologicalRules",[],{"meta":{"\/_B\/":"([.,!?\\s]|^)","\/_E\/":"([.,!?\\s]|$)"},"patterns":{"\/\u0001(.*)('|')s\u0001(?:'|')s(.*)\/":"\u0001$1$2s\u0001$3","\/_\u0001([^\u0001]*)\u0001\/":"javascript"}},1496],["IntlViewerContext",[],{"GENDER":3,"regionalLocale":null},772],["LinkshimHandlerConfig",[],{"supports_meta_referrer":false,"default_meta_referrer_policy":"default","switched_meta_referrer_policy":"origin","non_linkshim_lnfb_mode":"ie","link_react_default_hash":"AT1JQuFbLMetlNTFbiuYz03LHLfojo5_dYoCElhUV-juJ5KhfzBqs0x-iR9Z5Qca7Q_Gxbxvvy2ZlLT45CyHYmyFJLi2x_bJPlutYwSO9tsDub8Oly4s9XEGx30MMv3bAotD2wK8NM2JwCIu","untrusted_link_default_hash":"AT0i2SvvvcbaGVxHXHh5XhbCw8E_T7m9std7jolEbjOjqPF-Q1gg4jmMlgFtxhoxUh_NtBC9EDTT6wsCx66CA6BEi4KFsS957sPpcNd3E5BgbD0aj-KhDHwlsxt1lwwXvo5vsHrx16JTRPll","linkshim_host":"l.instagram.com","linkshim_path":"\/","linkshim_enc_param":"e","linkshim_url_param":"u","use_rel_no_opener":false,"use_rel_no_referrer":false,"always_use_https":false,"onion_always_shim":true,"middle_click_requires_event":false,"www_safe_js_mode":"hover","m_safe_js_mode":null,"ghl_param_link_shim":false,"click_ids":null,"is_linkshim_supported":false,"current_domain":"instagram.com","blocklisted_domains":["ad.doubleclick.net","ads-encryption-url-example.com","bs.serving-sys.com","ad.atdmt.com","adform.net","ad13.adfarm1.adition.com","ilovemyfreedoms.com","secure.adnxs.com"],"is_mobile_device":false},27],["NumberFormatConfig",[],{"decimalSeparator":".","numberDelimiter":",","minDigitsForThousandsSeparator":4,"standardDecimalPatternInfo":{"primaryGroupSize":3,"secondaryGroupSize":3},"numberingSystemData":null},54],["RelayAPIConfigDefaults",[],{"accessToken":"","actorID":"0","customHeaders":{"X-IG-App-ID":"936619743392459","X-IG-D":"www"},"enableNetworkLogger":false,"enableVerboseNetworkLogger":false,"fetchTimeout":30000,"graphURI":"\/api\/graphql\/","retryDelays":[1000,3000],"useXController":true,"xhrEncoding":null,"subscriptionTopicURI":null,"withCredentials":false,"isProductionEndpoint":false,"workRequestTaggingProduct":null,"encryptionKeyParams":null,"graphBatchURI":"\/api\/graphqlbatch\/"},926],["UserAgentData",[],{"browserArchitecture":"32","browserFullVersion":"8.7.1","browserMinorVersion":7,"browserName":"Curl","browserVersion":8,"deviceName":"Unknown","engineName":"Unknown","engineVersion":null,"platformArchitecture":"32","platformName":"Unknown","platformVersion":null,"platformFullVersion":null},527],["ZeroCategoryHeader",[],{},1127],["ZeroRewriteRules",[],{"rewrite_rules":{},"whitelist":{"\/hr\/r":1,"\/hr\/p":1,"\/zero\/unsupported_browser\/":1,"\/zero\/policy\/optin":1,"\/zero\/optin\/write\/":1,"\/zero\/optin\/legal\/":1,"\/zero\/optin\/free\/":1,"\/about\/privacy\/":1,"\/about\/privacy\/update\/":1,"\/privacy\/explanation\/":1,"\/zero\/toggle\/welcome\/":1,"\/zero\/toggle\/nux\/":1,"\/zero\/toggle\/settings\/":1,"\/fup\/interstitial\/":1,"\/work\/landing":1,"\/work\/login\/":1,"\/work\/email\/":1,"\/ai.php":1,"\/js_dialog_resources\/dialog_descriptions_android.json":0,"\/connect\/jsdialog\/MPlatformAppInvitesJSDialog\/":0,"\/connect\/jsdialog\/MPlatformOAuthShimJSDialog\/":0,"\/connect\/jsdialog\/MPlatformLikeJSDialog\/":0,"\/qp\/interstitial\/":1,"\/qp\/action\/redirect\/":1,"\/qp\/action\/close\/":1,"\/zero\/support\/ineligible\/":1,"\/zero_balance_redirect\/":1,"\/zero_balance_redirect":1,"\/zero_balance_redirect\/l\/":1,"\/l.php":1,"\/lsr.php":1,"\/ajax\/dtsg\/":1,"\/checkpoint\/block\/":1,"\/exitdsite":1,"\/zero\/balance\/pixel\/":1,"\/zero\/balance\/":1,"\/zero\/balance\/carrier_landing\/":1,"\/zero\/flex\/logging\/":1,"\/tr":1,"\/tr\/":1,"\/sem_campaigns\/sem_pixel_test\/":1,"\/bookmarks\/flyout\/body\/":1,"\/zero\/subno\/":1,"\/confirmemail.php":1,"\/policies\/":1,"\/mobile\/internetdotorg\/classifier\/":1,"\/zero\/dogfooding":1,"\/xti.php":1,"\/zero\/fblite\/config\/":1,"\/hr\/zsh\/wc\/":1,"\/ajax\/bootloader-endpoint\/":1,"\/mobile\/zero\/carrier_page\/":1,"\/mobile\/zero\/carrier_page\/education_page\/":1,"\/mobile\/zero\/carrier_page\/feature_switch\/":1,"\/mobile\/zero\/carrier_page\/settings_page\/":1,"\/aloha_check_build":1,"\/upsell\/zbd\/softnudge\/":1,"\/mobile\/zero\/af_transition\/":1,"\/mobile\/zero\/af_transition\/action\/":1,"\/mobile\/zero\/freemium\/":1,"\/mobile\/zero\/freemium\/redirect\/":1,"\/mobile\/zero\/freemium\/zero_fup\/":1,"\/privacy\/policy\/":1,"\/privacy\/center\/":1,"\/data\/manifest\/":1,"\/cmon":1,"\/cmon\/":1,"\/4oh4.php":1,"\/autologin.php":1,"\/birthday_help.php":1,"\/checkpoint\/":1,"\/contact-importer\/":1,"\/cr.php":1,"\/legal\/terms\/":1,"\/login.php":1,"\/login\/":1,"\/mobile\/account\/":1,"\/n\/":1,"\/remote_test_device\/":1,"\/upsell\/buy\/":1,"\/upsell\/buyconfirm\/":1,"\/upsell\/buyresult\/":1,"\/upsell\/promos\/":1,"\/upsell\/continue\/":1,"\/upsell\/h\/promos\/":1,"\/upsell\/loan\/learnmore\/":1,"\/upsell\/purchase\/":1,"\/upsell\/promos\/upgrade\/":1,"\/upsell\/buy_redirect\/":1,"\/upsell\/loan\/buyconfirm\/":1,"\/upsell\/loan\/buy\/":1,"\/upsell\/sms\/":1,"\/wap\/a\/channel\/reconnect.php":1,"\/wap\/a\/nux\/wizard\/nav.php":1,"\/wap\/appreg.php":1,"\/wap\/birthday_help.php":1,"\/wap\/c.php":1,"\/wap\/confirmemail.php":1,"\/wap\/cr.php":1,"\/wap\/login.php":1,"\/wap\/r.php":1,"\/zero\/datapolicy":1,"\/a\/timezone.php":1,"\/a\/bz":1,"\/bz\/reliability":1,"\/r.php":1,"\/mr\/":1,"\/reg\/":1,"\/registration\/log\/":1,"\/terms\/":1,"\/f123\/":1,"\/expert\/":1,"\/experts\/":1,"\/terms\/index.php":1,"\/terms.php":1,"\/srr\/":1,"\/msite\/redirect\/":1,"\/fbs\/pixel\/":1,"\/contactpoint\/preconfirmation\/":1,"\/contactpoint\/cliff\/":1,"\/contactpoint\/confirm\/submit\/":1,"\/contactpoint\/confirmed\/":1,"\/contactpoint\/login\/":1,"\/preconfirmation\/contactpoint_change\/":1,"\/help\/contact\/":1,"\/survey\/":1,"\/upsell\/loyaltytopup\/accept\/":1,"\/settings\/":1,"\/lite\/":1,"\/zero_status_update\/":1,"\/operator_store\/":1,"\/upsell\/":1,"\/wifiauth\/login\/":1}},1478],["CookieCoreLoggingConfig",[],{"maximumIgnorableStallMs":16.67,"sampleRate":9.7e-5,"sampleRateClassic":1.0e-10,"sampleRateFastStale":1.0e-8},3401],["FBDomainsSVConfig",[],{"domains":{"__map":[["www.facebook.com",1],["tfbnw.net",1],["m.beta.facebook.com",1],["touch.beta.facebook.com",1],["www.dev.facebook.com",1],["fb.me",1],["s.fb.com",1],["m.fbjs.facebook.com",1],["facebook.com.es",1],["www.fbjs.facebook.com",1],["m.facebook.com",1],["facebook.fr",1],["fbsbx.com",1],["embed.fbsbx.com",1],["attachment.fbsbx.com",1],["lookaside.fbsbx.com",1],["web.facebook.com",1],["fb.com",1],["messenger.com",1],["secure.facebook.com",1],["secure.my-od.facebook.com",1],["www.my-od.facebook.com",1]]}},3828],["ClickIDDomainBlacklistSVConfig",[],{"domains":["craigslist","tfbnw.net","canadiantire.ca","o2.co.uk","archive.org","reddit.com","redd.it","gmail.com","cvk.gov.ua","electoralsearch.in","yahoo.com","cve.mitre.org","usenix.org","ky.gov","voteohio.gov","vote.pa.gov","oversightboard.com","wi.gov","pbs.twimg.com","media.discordapp.net","vastadeal.com","theaustralian.com.au","alloygator.com","elsmannimmobilien.de","news.com.au","dennisbonnen.com","stoett.com","investorhour.com","perspectivasur.com","bonnegueule.fr","firstent.org","twitpic.com","kollosche.com.au","nau.edu","arcourts.gov","lomberg.de","network4.hu","balloonrace.com","awstrack.me","ic3.gov","sos.wyo.gov","cnpq.br","0.discoverapp.com","apple.com","apple.co","applecard.apple","services.apple","appletvplus.com","applepay.apple","wallet.apple","beatsbydre.com","dinn.com.mx","soriana.com","facebook.sso.datasite.com","fycextras.com","rik.parlament.gov.rs","elections.delaware.gov","dge.sn","facebook.co1.qualtrics.com","instagram.qualtrics.com"]},3829],["CometCustomKeyCommands",[],{"customCommands":{},"areSingleKeysDisabled":null,"modifiedKeyboardShortcutsPreference":4},4521],["CometRelayConfig",[],{"gc_release_buffer_size":50},4685],["TimeSpentWWWCometConfig",[],{"CONFIG":{"0_delay":0,"0_timeout":8,"delay":1000,"timeout":64}},4748],["CometMaxEnqueuedToastsSitevarConfig",[],{"max":2},4763],["QuickMarkersConfig",[],{"pageLoadEventId":"7483563829649516416","pageLoadScriptPath":"XPolarisPostController","sampleWeight":null},4953],["ServerTimeData",[],{"serverTime":1742402983550,"timeOfRequestStart":1742402983459.5,"timeOfResponseStart":1742402983533.7},5943],["InstagramUserAgent",[],{"is_chrome":false,"is_edge":false,"is_edge_chromium_based":false,"is_edge_legacy":false,"is_firefox":false,"is_ig_carbon":false,"is_ig_lite":false,"is_ig_webview":false,"is_barcelona_webview":false,"is_igtv_webview":false,"is_in_app_browser":false,"is_ios":false,"is_android":false,"is_windows_nt":false,"is_ipad":false,"is_macos":false,"is_mobile":false,"is_mobile_safari":false,"is_oculus_browser":false,"is_opera":false,"is_safari":false,"is_supported_browser":false,"is_twitter_webview":false,"is_uc_browser":false,"is_vapid_eligible":false,"is_webview":false,"is_windows_pwa":false,"is_igvr":false,"user_agent":"curl\/8.7.1"},6088],["WebBloksVersioningID",[],{"versioningID":"e7c968a852c20a5809af0f132c00310b0f6e633456328181b298a8955b4764e8"},6636],["PolarisPrivacyFlowTrigger",[],{"data":null},7462],["PolarisLocales",[],{"supported_locales":{"af_ZA":{"primary_code":"af","english_name":"Afrikaans","display_name":"Afrikaans"},"ar_AR":{"primary_code":"ar","english_name":"Arabic","display_name":"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},"cs_CZ":{"primary_code":"cs","english_name":"Czech","display_name":"\u010ce\u0161tina"},"da_DK":{"primary_code":"da","english_name":"Danish","display_name":"Dansk"},"de_DE":{"primary_code":"de","english_name":"German","display_name":"Deutsch"},"el_GR":{"primary_code":"el","english_name":"Greek","display_name":"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},"en_US":{"primary_code":"en","english_name":"English","display_name":"English"},"en_GB":{"primary_code":"en-gb","english_name":"English (UK)","display_name":"English (UK)"},"es_ES":{"primary_code":"es","english_name":"Spanish","display_name":"Espa\u00f1ol (Espa\u00f1a)"},"es_LA":{"primary_code":"es-la","english_name":"Spanish","display_name":"Espa\u00f1ol"},"fa_IR":{"primary_code":"fa","english_name":"Persian","display_name":"\u0641\u0627\u0631\u0633\u06cc"},"fi_FI":{"primary_code":"fi","english_name":"Finnish","display_name":"Suomi"},"fr_FR":{"primary_code":"fr","english_name":"French","display_name":"Fran\u00e7ais"},"he_IL":{"primary_code":"he","english_name":"Hebrew","display_name":"\u05e2\u05d1\u05e8\u05d9\u05ea"},"id_ID":{"primary_code":"id","english_name":"Indonesian","display_name":"Bahasa Indonesia"},"it_IT":{"primary_code":"it","english_name":"Italian","display_name":"Italiano"},"ja_JP":{"primary_code":"ja","english_name":"Japanese","display_name":"\u65e5\u672c\u8a9e"},"ko_KR":{"primary_code":"ko","english_name":"Korean","display_name":"\ud55c\uad6d\uc5b4"},"ms_MY":{"primary_code":"ms","english_name":"Malay","display_name":"Bahasa Melayu"},"nb_NO":{"primary_code":"nb","english_name":"Norwegian","display_name":"Norsk"},"nl_NL":{"primary_code":"nl","english_name":"Dutch","display_name":"Nederlands"},"pl_PL":{"primary_code":"pl","english_name":"Polish","display_name":"Polski"},"pt_BR":{"primary_code":"pt-br","english_name":"Portuguese (Brazil)","display_name":"Portugu\u00eas (Brasil)"},"pt_PT":{"primary_code":"pt","english_name":"Portuguese (Portugal)","display_name":"Portugu\u00eas (Portugal)"},"ru_RU":{"primary_code":"ru","english_name":"Russian","display_name":"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},"sv_SE":{"primary_code":"sv","english_name":"Swedish","display_name":"Svenska"},"th_TH":{"primary_code":"th","english_name":"Thai","display_name":"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},"tl_PH":{"primary_code":"tl","english_name":"Tagalog\/Filipino","display_name":"Filipino"},"tr_TR":{"primary_code":"tr","english_name":"Turkish","display_name":"T\u00fcrk\u00e7e"},"zh_CN":{"primary_code":"zh-cn","english_name":"Simplified Chinese (China)","display_name":"\u4e2d\u6587(\u7b80\u4f53)"},"zh_TW":{"primary_code":"zh-tw","english_name":"Traditional Chinese (Taiwan)","display_name":"\u4e2d\u6587(\u53f0\u7063)"},"bn_IN":{"primary_code":"bn","english_name":"Bengali","display_name":"\u09ac\u09be\u0982\u09b2\u09be"},"gu_IN":{"primary_code":"gu","english_name":"Gujarati","display_name":"\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0"},"hi_IN":{"primary_code":"hi","english_name":"Hindi","display_name":"\u0939\u093f\u0928\u094d\u0926\u0940"},"hr_HR":{"primary_code":"hr","english_name":"Croatian","display_name":"Hrvatski"},"hu_HU":{"primary_code":"hu","english_name":"Hungarian","display_name":"Magyar"},"kn_IN":{"primary_code":"kn","english_name":"Kannada","display_name":"\u0c95\u0ca8\u0ccd\u0ca8\u0ca1"},"ml_IN":{"primary_code":"ml","english_name":"Malayalam","display_name":"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},"mr_IN":{"primary_code":"mr","english_name":"Marathi","display_name":"\u092e\u0930\u093e\u0920\u0940"},"ne_NP":{"primary_code":"ne","english_name":"Nepali","display_name":"\u0928\u0947\u092a\u093e\u0932\u0940"},"pa_IN":{"primary_code":"pa","english_name":"Punjabi","display_name":"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},"si_LK":{"primary_code":"si","english_name":"Sinhala","display_name":"\u0dc3\u0dd2\u0d82\u0dc4\u0dbd"},"sk_SK":{"primary_code":"sk","english_name":"Slovak","display_name":"Sloven\u010dina"},"ta_IN":{"primary_code":"ta","english_name":"Tamil","display_name":"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},"te_IN":{"primary_code":"te","english_name":"Telugu","display_name":"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},"ur_PK":{"primary_code":"ur","english_name":"Urdu","display_name":"\u0627\u0631\u062f\u0648"},"vi_VN":{"primary_code":"vi","english_name":"Vietnamese","display_name":"Ti\u1ebfng Vi\u1ec7t"},"zh_HK":{"primary_code":"zh-hk","english_name":"Traditional Chinese (Hong Kong)","display_name":"\u4e2d\u6587(\u9999\u6e2f)"},"bg_BG":{"primary_code":"bg","english_name":"Bulgarian","display_name":"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},"fr_CA":{"primary_code":"fr-ca","english_name":"French (Canada)","display_name":"Fran\u00e7ais (Canada)"},"ro_RO":{"primary_code":"ro","english_name":"Romanian","display_name":"Rom\u00e2n\u0103"},"sr_RS":{"primary_code":"sr","english_name":"Serbian","display_name":"\u0421\u0440\u043f\u0441\u043a\u0438"},"uk_UA":{"primary_code":"uk","english_name":"Ukrainian","display_name":"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"}},"locale":"en_US","language_code":"en"},7220],["PolarisCookieConsent",[],{"should_show_consent_dialog":false},7329],["DspFDSWebLegacyThemeUsage",[],{"light":{"accent":"#0866FF","blue-link":"#0064D1","fb-wordmark":"#0866FF","filter-accent":"invert(19\u0025) sepia(70\u0025) saturate(5671\u0025) hue-rotate(203deg) brightness(96\u0025) contrast(101\u0025)","filter-primary-accent":"invert(27\u0025) sepia(95\u0025) saturate(3116\u0025) hue-rotate(212deg) brightness(99\u0025) contrast(105\u0025)","focus-ring-blue":"#0866FF","hosted-view-selected-state":"#EBF5FF","input-label-color-highlighted":"#0064D1","primary-button-background":"#0866FF","primary-deemphasized-button-background":"#EBF5FF","primary-deemphasized-button-text":"#0064D1","progress-ring-blue-background":"#0866FF33","progress-ring-blue-foreground":"#0866FF","text-highlight":"#0866FF33","toggle-active-background":"#EBF5FF","toggle-active-icon":"#0064D1","toggle-active-text":"#0064D1"},"dark":{"accent":"#0866FF","blue-link":"#5AA7FF","filter-accent":"invert(74\u0025) sepia(14\u0025) saturate(7129\u0025) hue-rotate(185deg) brightness(102\u0025) contrast(101\u0025)","filter-primary-accent":"invert(27\u0025) sepia(95\u0025) saturate(3116\u0025) hue-rotate(212deg) brightness(99\u0025) contrast(105\u0025)","focus-ring-blue":"#1D85FC","hosted-view-selected-state":"#1D85FC19","input-label-color-highlighted":"#3E93F8","primary-button-background":"#0866FF","primary-deemphasized-button-background":"#1D85FC33","primary-deemphasized-button-text":"#75B6FF","progress-ring-blue-background":"#0866FF33","progress-ring-blue-foreground":"#0866FF","text-highlight":"#1D85FC72","toggle-active-background":"#1D85FC"}},7331],["InstagramWebPushInfo",[],{"bundle_variant":"wwwig","deployment_stage":"C3","frontend_env":"C3","is_on_vpn":false,"rollout_hash":"1021044255"},7332],["InstagramPasswordEncryption",[],{"key_id":"154","public_key":"d003df5911d389cb6fdcaa8065ca886ac010d3cbb128017c746a0839d2771c34","version":"10"},7339],["InstagramSEOCrawlBot",[],{"is_allowlisted_crawl_bot":false,"is_google_crawl_bot":false,"is_crawler_with_ssr":false,"is_crawler_with_relay":false,"username_to_set_lookaside_url_for_profile_picture":null,"should_hide_upsells_for_seo_crawlers":false,"use_lookaside_for_post_media":false,"should_disable_js_fetching_posts_on_profile":false,"distillery_domain_is_lookaside":false,"should_render_full_name_for_media":false,"should_render_single_h1":false,"should_render_caption_as_title":false,"should_render_post_header_with_fullname":false,"post_translated_text":null,"post_translated_text_media_id":null,"should_log_page_view_immediately":false,"reels_lookaside_url":null},7340],["PolarisViewer",[],{"data":null,"id":null},7365],["IGCookieSettingsLoggedOutConfig",[],{"should_show_cookie_settings":false},7434],["InstagramSecurityConfig",[],{"csrf_token":"LI0zjxxV23L5r5wlE7JNfW"},7467],["PolarisLoggedOutUpsellExposureV2",[],{"desktop":{},"mweb":{"show_upsells_all_landing_pages":false},"xplatform":{"show_blocking_content_wall_for_all_content":false,"show_sign_up_focused_blocking_content_wall":false}},7946],["PolarisExperimentUtils",[],{"is_logged_out_user":true,"is_logged_out_user_ssr":true,"use_landing_dialog_v2":false,"landing_dialog_v2_variant":0},7968],["DateFormatConfig",[],{"numericDateOrder":["m","d","y"],"numericDateSeparator":"\/","shortDayNames":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],"narrowDayNames":["M","T","W","T","F","S","S"],"timeSeparator":":","weekStart":6,"formats":{"D":"D","D g:ia":"D g:ia","D M d":"D M d","D M d, Y":"D M d, Y","D M j":"D M j","D M j, g:ia":"D M j, g:ia","D M j, y":"D M j, y","D M j, Y g:ia":"D M j, Y g:ia","D, M j, Y":"D, M j, Y","F d":"F d","F d, Y":"F d, Y","F g":"F g","F j":"F j","F j, Y":"F j, Y","F j, Y \u0040 g:i A":"F j, Y \u0040 g:i A","F j, Y g:i a":"F j, Y g:i a","F jS":"F jS","F jS, g:ia":"F jS, g:ia","F jS, Y":"F jS, Y","F Y":"F Y","g A":"g A","g:i":"g:i","g:i A":"g:i A","g:i a":"g:i a","g:iA":"g:iA","g:ia":"g:ia","g:ia F jS, Y":"g:ia F jS, Y","g:iA l, F jS":"g:iA l, F jS","g:ia M j":"g:ia M j","g:ia M jS":"g:ia M jS","g:ia, F jS":"g:ia, F jS","g:iA, l M jS":"g:iA, l M jS","g:sa":"g:sa","H:I - M d, Y":"H:I - M d, Y","h:i a":"h:i a","h:m:s m\/d\/Y":"h:m:s m\/d\/Y","j":"j","l F d, Y":"l F d, Y","l g:ia":"l g:ia","l, F d, Y":"l, F d, Y","l, F j":"l, F j","l, F j, Y":"l, F j, Y","l, F jS":"l, F jS","l, F jS, g:ia":"l, F jS, g:ia","l, M j":"l, M j","l, M j, Y":"l, M j, Y","l, M j, Y g:ia":"l, M j, Y g:ia","M d":"M d","M d, Y":"M d, Y","M d, Y g:ia":"M d, Y g:ia","M d, Y ga":"M d, Y ga","M j":"M j","M j, Y":"M j, Y","M j, Y g:i A":"M j, Y g:i A","M j, Y g:ia":"M j, Y g:ia","M jS, g:ia":"M jS, g:ia","M Y":"M Y","M y":"M y","m-d-y":"m-d-y","M. d":"M. d","M. d, Y":"M. d, Y","j F Y":"j F Y","m.d.y":"m.d.y","m\/d":"m\/d","m\/d\/Y":"m\/d\/Y","m\/d\/y":"m\/d\/y","m\/d\/Y g:ia":"m\/d\/Y g:ia","m\/d\/y H:i:s":"m\/d\/y H:i:s","m\/d\/Y h:m":"m\/d\/Y h:m","n":"n","n\/j":"n\/j","n\/j, g:ia":"n\/j, g:ia","n\/j\/y":"n\/j\/y","Y":"Y","Y-m-d":"Y-m-d","Y\/m\/d":"Y\/m\/d","y\/m\/d":"y\/m\/d","j \/ F \/ Y":"j \/ F \/ Y"},"ordinalSuffixes":{"1":"st","2":"nd","3":"rd","4":"th","5":"th","6":"th","7":"th","8":"th","9":"th","10":"th","11":"th","12":"th","13":"th","14":"th","15":"th","16":"th","17":"th","18":"th","19":"th","20":"th","21":"st","22":"nd","23":"rd","24":"th","25":"th","26":"th","27":"th","28":"th","29":"th","30":"th","31":"st"}},165],["BrowserPushPubKey",[],{"appServerKey":"BIBn3E_rWTci8Xn6P9Xj3btShT85Wdtne0LtwNUyRQ5XjFNkuTq9j4MPAVLvAFhXrUU1A9UxyxBA7YIOjqDIDHI"},4806],["WebDriverConfig",[],{"isTestRunning":false,"isJestE2ETestRun":false,"isXRequestConfigEnabled":false,"auxiliaryServiceInfo":{},"testPath":null,"originHost":null},5332],["cr:273",["FocusWithinHandlerStrictMode.react"],{"__rc":["FocusWithinHandlerStrictMode.react",null]},-1],["cr:1094133",[],{"__rc":[null,null]},-1],["cr:2682",["warningComet"],{"__rc":["warningComet",null]},-1],["cr:11202",[],{"__rc":[null,null]},-1],["cr:1105154",[],{"__rc":[null,null]},-1],["cr:5277",["ReactDOM.classic.prod-or-profiling"],{"__rc":["ReactDOM.classic.prod-or-profiling",null]},-1],["cr:13683",[],{"__rc":[null,null]},-1],["cr:9610",[],{"__rc":[null,null]},-1],["cr:1642797",["BanzaiComet"],{"__rc":["BanzaiComet",null]},-1],["cr:2548",["CometRelayEnvironmentWWW"],{"__rc":["CometRelayEnvironmentWWW",null]},-1],["cr:3096",[],{"__rc":[null,null]},-1],["cr:5888",[],{"__rc":[null,null]},-1],["cr:1183579",["InlineFbtResultImplComet"],{"__rc":["InlineFbtResultImplComet",null]},-1],["cr:3798",[],{"__rc":[null,null]},-1],["cr:1024",[],{"__rc":[null,null]},-1],["cr:1829844",[],{"__rc":[null,null]},-1],["cr:1292365",["React-prod.classic"],{"__rc":["React-prod.classic",null]},-1],["cr:6895",["cometAsyncRequestHeaders"],{"__rc":["cometAsyncRequestHeaders",null]},-1],["cr:3376",["cometAsyncFetchShared"],{"__rc":["cometAsyncFetchShared",null]},-1],["cr:5621",["CometLinkOldImpl.react"],{"__rc":["CometLinkOldImpl.react",null]},-1],["cr:1083116",[],{"__rc":[null,null]},-1],["cr:1083117",["recoverableViolation"],{"__rc":["recoverableViolation",null]},-1],["cr:8736",["IgLoggedOutWebCtaClickFalcoEvent"],{"__rc":["IgLoggedOutWebCtaClickFalcoEvent",null]},-1],["cr:4477",["PolarisDesktopNavLoggedOutContainer.react"],{"__rc":["PolarisDesktopNavLoggedOutContainer.react",null]},-1],["cr:99",[],{"__rc":[null,null]},-1],["cr:160",["MessengerWebUXLoggerImpl"],{"__rc":["MessengerWebUXLoggerImpl",null]},-1],["cr:355",[],{"__rc":[null,null]},-1],["cr:499",[],{"__rc":[null,null]},-1],["cr:506",[],{"__rc":[null,null]},-1],["cr:567",["SaharaCometConsentFlowContextualConsentFallbackLoadingStateIGDS.react"],{"__rc":["SaharaCometConsentFlowContextualConsentFallbackLoadingStateIGDS.react",null]},-1],["cr:913",[],{"__rc":[null,null]},-1],["cr:1342",[],{"__rc":[null,null]},-1],["cr:1358",["usePolarisOzImplementation"],{"__rc":["usePolarisOzImplementation",null]},-1],["cr:2136",[],{"__rc":[null,null]},-1],["cr:2336",[],{"__rc":[null,null]},-1],["cr:4149",[],{"__rc":[null,null]},-1],["cr:4596",[],{"__rc":[null,null]},-1],["cr:4877",["usePolarisGNVReporter"],{"__rc":["usePolarisGNVReporter",null]},-1],["cr:5000",["PolarisLoggedOutSidecarUpsell.react"],{"__rc":["PolarisLoggedOutSidecarUpsell.react",null]},-1],["cr:6094",["useEmptyFunction"],{"__rc":["useEmptyFunction",null]},-1],["cr:6101",["useThrottledImpl"],{"__rc":["useThrottledImpl",null]},-1],["cr:6269",["GHLSVGTextWithTspanElement.react"],{"__rc":["GHLSVGTextWithTspanElement.react",null]},-1],["cr:6866",["PolarisLoggedOutVideo.react"],{"__rc":["PolarisLoggedOutVideo.react",null]},-1],["cr:7175",[],{"__rc":[null,null]},-1],["cr:8735",[],{"__rc":[null,null]},-1],["cr:8817",["IgLoggedOutWebCtaImpressionFalcoEvent"],{"__rc":["IgLoggedOutWebCtaImpressionFalcoEvent",null]},-1],["cr:9661",["FXFetaAutoMigrationPromptDialogRootQuery$Parameters.instagram"],{"__rc":["FXFetaAutoMigrationPromptDialogRootQuery$Parameters.instagram",null]},-1],["cr:9693",["FXFetaPreMigrationDialogRootQuery$Parameters.instagram"],{"__rc":["FXFetaPreMigrationDialogRootQuery$Parameters.instagram",null]},-1],["cr:9719",["FXFetaToSDialogRootQuery$Parameters.instagram"],{"__rc":["FXFetaToSDialogRootQuery$Parameters.instagram",null]},-1],["cr:10818",[],{"__rc":[null,null]},-1],["cr:13141",["IGWebSession"],{"__rc":["IGWebSession",null]},-1],["cr:964538",[],{"__rc":[null,null]},-1],["cr:1073372",["useOnBeforeUnloadComet"],{"__rc":["useOnBeforeUnloadComet",null]},-1],["cr:1453865",[],{"__rc":[null,null]},-1],["cr:1473550",[],{"__rc":[null,null]},-1],["cr:1672302",[],{"__rc":[null,null]},-1],["cr:1680308",[],{"__rc":[null,null]},-1],["cr:1724253",[],{"__rc":[null,null]},-1],["cr:1824699",["DebugOwl"],{"__rc":["DebugOwl",null]},-1],["cr:1954434",[],{"__rc":[null,null]},-1],["cr:2010754",[],{"__rc":[null,null]},-1],["cr:7086",[],{"__rc":[null,null]},-1],["cr:8737",["IgLoggedOutWebCtaDismissFalcoEvent"],{"__rc":["IgLoggedOutWebCtaDismissFalcoEvent",null]},-1],["cr:694370",["requestIdleCallbackComet"],{"__rc":["requestIdleCallbackComet",null]},-1],["cr:795",[],{"__rc":[null,null]},-1],["cr:1565",[],{"__rc":[null,null]},-1],["cr:1588",[],{"__rc":[null,null]},-1],["cr:1618",[],{"__rc":[null,null]},-1],["cr:4506",["PolarisKeyCommandNub.react"],{"__rc":["PolarisKeyCommandNub.react",null]},-1],["cr:4878",["PolarisGNVManagerProvider.react"],{"__rc":["PolarisGNVManagerProvider.react",null]},-1],["cr:5286",[],{"__rc":[null,null]},-1],["cr:5630",["bootstrapPolarisPreloader"],{"__rc":["bootstrapPolarisPreloader",null]},-1],["cr:5631",[],{"__rc":[null,null]},-1],["cr:6013",[],{"__rc":[null,null]},-1],["cr:6916",[],{"__rc":[null,null]},-1],["cr:9431",["PolarisBugReportProvider.react"],{"__rc":["PolarisBugReportProvider.react",null]},-1],["cr:10569",[],{"__rc":[null,null]},-1],["cr:11053",[],{"__rc":[null,null]},-1],["cr:13873",["FDSCrossFilled20PNGIcon.react"],{"__rc":["FDSCrossFilled20PNGIcon.react",null]},-1],["cr:14159",["cometRouterEmptyLogger"],{"__rc":["cometRouterEmptyLogger",null]},-1],["cr:992073",[],{"__rc":[null,null]},-1],["cr:1119068",[],{"__rc":[null,null]},-1],["cr:1813330",[],{"__rc":[null,null]},-1],["WebLoomConfig",[],{"adaptive_config":{"interactions":{"modules":{"8448":1},"events":{"553648129.comet.fx.accounts_center.home":1.5,"553648129.polaris.CAAIGLoginHomepage":5.4,"553648129.polaris.DirectInboxPage":96.1,"553648129.polaris.DirectThreadPage":17.4,"553648129.polaris.FXCalReauthLoginForm":1.3,"553648129.polaris.OneTapUpsellPage":40.8,"553648129.polaris.StoriesPage":28.1,"553648129.polaris.UFAC":4.2,"553648129.polaris.UniversalHackedLockSelectAccountPage":1.1,"553648129.polaris.challenge":36.7,"553648129.polaris.editProfile":3.1,"553648129.polaris.exploreLandingPage":10.5,"553648129.polaris.fbSignupPage":4.7,"553648129.polaris.feedPage":537.2,"553648129.polaris.httpErrorPage":33.4,"553648129.polaris.loginPage":99.4,"553648129.polaris.multiStepSignupPage":1.3,"553648129.polaris.permDisableScreen":3.2,"553648129.polaris.postExternalSharingLanding":79,"553648129.polaris.postPage":572.2,"553648129.polaris.privatePostPage":1.2,"553648129.polaris.profilePage":517.4,"553648129.polaris.reelsTab":11.9,"553648129.polaris.resetPassword":5.9,"553648129.polaris.unifiedHome":121.6,"553648130.comet.fx.accounts_center.home":1.1,"553648130.comet.fx.accounts_center.section":3.6,"553648130.polaris.ActivityFeedPage":23.8,"553648130.polaris.ClipsAudioPage":1.4,"553648130.polaris.DirectInboxPage":1048,"553648130.polaris.DirectRequestPage":11.1,"553648130.polaris.KeywordSearchExplorePage":6.5,"553648130.polaris.SavedCollectionPage":17.1,"553648130.polaris.StoriesPage":1300.4,"553648130.polaris.TwoFactorPage":1.3,"553648130.polaris.accountSettingsPage":9.2,"553648130.polaris.archiveStories":2.8,"553648130.polaris.editProfile":3.9,"553648130.polaris.exploreLandingPage":131.1,"553648130.polaris.exploreSearchPage":39.2,"553648130.polaris.feedPage":651.1,"553648130.polaris.followList":18.9,"553648130.polaris.locationPage":1.4,"553648130.polaris.loginPage":6.1,"553648130.polaris.mobileAllCommentsPage":23.3,"553648130.polaris.multiStepSignupPage":14.7,"553648130.polaris.postPage":121.8,"553648130.polaris.profilePage":756.2,"553648130.polaris.reelsTab":38,"553648130.polaris.resetPassword":4,"553648130.polaris.settingsV2":1.1,"553648130.polaris.unifiedHome":2.7,"553648130.polaris.yourActivityInteractionsPage":1.8,"553655735.comet.dialog.CAAFetaSavePasswordInterstitialDialog.react":9.3,"553655735.comet.dialog.IGDChatSettingsDeleteThreadDialog.react":6.2,"553655735.comet.dialog.IGDMediaViewer.react":40.8,"553655735.comet.dialog.PolarisClipsViewerCommentsSheet.next.react":2.2,"553655735.comet.dialog.PolarisLoggedOutContentWallDialog.react":91.7,"553655735.comet.dialog.PolarisLoggedOutIntentDialog.react":1.6,"553655735.comet.dialog.PolarisPostLikedByListDialogRoot.react":5.5,"553655735.comet.dialog.PolarisProfileOwnOptionsDialog.react":4,"553655735.comet.dialog.PolarisShareMenuDialog.react":4.6,"553655735.comet.dialog.PolarisShareSheetV3Root.react":41.9,"553655735.igds.modal.IGDEditThreadNameDialog.react":81.9,"553655735.igds.modal.IGDMessageUnsendDialog.react":21.2,"553655735.igds.modal.IGDSecureShareSheetDialog.react":7.4,"553655735.igds.modal.IGDThreadListNewMessageDialog.react":3.9,"553655735.igds.modal.PolarisAboutThisAccountDialog.next.react":4.1,"553655735.igds.modal.PolarisCreationModal.next.react":6.4,"553655735.igds.modal.PolarisFollowListModalWrapper.react":30.1,"553655735.igds.modal.PolarisFollowingActionsModal.next.react":4.5,"553655735.igds.modal.PolarisLoggingOutDialog.react":1.1,"553655735.igds.modal.PolarisProfileOtherOptionsDialog.react":8.1,"553655735.igds.modal.PolarisProfilePageMultipleLinksModal.react":3.3,"553655735.igds.modal.PolarisStoriesV3ReelOptionsDialog.react":4,"553655735.igds.modal.PolarisStoriesV3ViewerListDialog.react":13.8,"553655735.igds.modal.PolarisUnfollowDialog.react":14.6,"553655735.igds.popover.IGDComposerEmojiPicker.react":102.6,"553655735.igds.popover.IGDMessageContextMenu.react":41.5,"553655735.igds.popover.PolarisClipsDesktopCommentsPopover.react":54.5,"553655735.igds.popover.PolarisEmojiPopover.react":2.3,"553655735.igds.popover.PolarisInboxTrayItemPopover.react":22.2,"553655735.igds.popover.PolarisMobileCreationMenuPopover.react":1.1,"553655735.igds.popover.PolarisMoreContextMenu.react":20,"553655735.igds.popover.PolarisStoriesV3FeedMediaStickerPopover.react":16.5,"553655735.igds.popover.PolarisStoriesV3LinkStickerPopover.react":3.4,"553655735.igds.popover.PolarisStoriesV3MentionStickerPopover.react":13.9,"553655735.mwp.dialog.IGDReactionsDialog.react":2.8}},"qpl":{"modules":{},"events":{}},"modules":null,"events":null}},4171],["NewsRegulationErrorMessageData",[],{"availableErrorCodes":[2216007,2216012,2216012],"errorCodeToRegType":{"2216007":"c18","2216012":"au00"},"learnMoreLinks":{"c18":{"regulated_user":"https:\/\/www.facebook.com\/help\/787040499275067","user":"https:\/\/www.facebook.com\/help\/2579891418969617"},"au00":{"regulated_user":"https:\/\/www.facebook.com\/help\/787040499275067","user":"https:\/\/www.facebook.com\/help\/2579891418969617"},"global_block":{"regulated_user":"https:\/\/www.facebook.com\/help\/787040499275067","user":"https:\/\/www.facebook.com\/help\/2579891418969617"}},"learnMoreLink":{"regulated_user":"https:\/\/www.facebook.com\/help\/787040499275067","user":"https:\/\/www.facebook.com\/help\/2579891418969617"},"appealLinks":{"c18":"https:\/\/www.facebook.com\/help\/contact\/419859403337390"}},7133],["FbtResultGK",[],{"shouldReturnFbtResult":true,"inlineMode":"NO_INLINE"},876],["ImmediateActiveSecondsConfig",[],{"sampling_rate":0},423],["CurrencyConfig",[],{"adsCurrenciesByCode":{"AED":{"iso":"AED","format":"{symbol}{amount}","symbol":"\u062f.\u0625","offset":100,"name":"UAE Dirham"},"ARS":{"iso":"ARS","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Argentine Peso"},"AUD":{"iso":"AUD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Australian Dollar"},"BDT":{"iso":"BDT","format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"name":"Bangladeshi Taka"},"BOB":{"iso":"BOB","format":"{symbol}{amount}","symbol":"Bs.","offset":100,"name":"Bolivian Boliviano"},"BRL":{"iso":"BRL","format":"{symbol}{amount}","symbol":"R$","offset":100,"name":"Brazilian Real"},"CAD":{"iso":"CAD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Canadian Dollar"},"CHF":{"iso":"CHF","format":"{symbol}{amount}","symbol":"Fr.","offset":100,"name":"Swiss Franc"},"CLP":{"iso":"CLP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Chilean Peso"},"CNY":{"iso":"CNY","format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"name":"Chinese Yuan"},"COP":{"iso":"COP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Colombian Peso"},"CRC":{"iso":"CRC","format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"name":"Costa Rican Col\u00f3n"},"CZK":{"iso":"CZK","format":"{symbol}{amount}","symbol":"K\u010d","offset":100,"name":"Czech Koruna"},"DKK":{"iso":"DKK","format":"{symbol}{amount}","symbol":"kr.","offset":100,"name":"Danish Krone"},"DZD":{"iso":"DZD","format":"{symbol}{amount}","symbol":"DA","offset":100,"name":"Algerian Dinar"},"EGP":{"iso":"EGP","format":"{symbol}{amount}","symbol":"\u062c.\u0645.","offset":100,"name":"Egyptian Pound"},"EUR":{"iso":"EUR","format":"{symbol}{amount}","symbol":"\u20ac","offset":100,"name":"Euro"},"GBP":{"iso":"GBP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"British Pound"},"GTQ":{"iso":"GTQ","format":"{symbol}{amount}","symbol":"Q","offset":100,"name":"Guatemalan Quetzal"},"HKD":{"iso":"HKD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Hong Kong Dollar"},"HNL":{"iso":"HNL","format":"{symbol}{amount}","symbol":"L.","offset":100,"name":"Honduran Lempira"},"HUF":{"iso":"HUF","format":"{symbol}{amount}","symbol":"Ft","offset":1,"name":"Hungarian Forint"},"IDR":{"iso":"IDR","format":"{symbol}{amount}","symbol":"Rp","offset":1,"name":"Indonesian Rupiah"},"ILS":{"iso":"ILS","format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"name":"Israeli New Shekel"},"INR":{"iso":"INR","format":"{symbol}{amount}","symbol":"\u20b9","offset":100,"name":"Indian Rupee"},"ISK":{"iso":"ISK","format":"{symbol}{amount}","symbol":"kr.","offset":1,"name":"Iceland Krona"},"JPY":{"iso":"JPY","format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"name":"Japanese Yen"},"KES":{"iso":"KES","format":"{symbol}{amount}","symbol":"KSh","offset":100,"name":"Kenyan Shilling"},"KRW":{"iso":"KRW","format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"name":"Korean Won"},"MOP":{"iso":"MOP","format":"{symbol}{amount}","symbol":"MOP","offset":100,"name":"Macau Patacas"},"MXN":{"iso":"MXN","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Mexican Peso"},"MYR":{"iso":"MYR","format":"{symbol}{amount}","symbol":"RM","offset":100,"name":"Malaysian Ringgit"},"NGN":{"iso":"NGN","format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"name":"Nigerian Naira"},"NIO":{"iso":"NIO","format":"{symbol}{amount}","symbol":"C$","offset":100,"name":"Nicaraguan Cordoba"},"NOK":{"iso":"NOK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Norwegian Krone"},"NZD":{"iso":"NZD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"New Zealand Dollar"},"PEN":{"iso":"PEN","format":"{symbol}{amount}","symbol":"S\/","offset":100,"name":"Peruvian Nuevo Sol"},"PHP":{"iso":"PHP","format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"name":"Philippine Peso"},"PKR":{"iso":"PKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Pakistani Rupee"},"PLN":{"iso":"PLN","format":"{symbol}{amount}","symbol":"z\u0142","offset":100,"name":"Polish Zloty"},"PYG":{"iso":"PYG","format":"{symbol}{amount}","symbol":"\u20b2","offset":1,"name":"Paraguayan Guarani"},"QAR":{"iso":"QAR","format":"{symbol}{amount}","symbol":"\u0631.\u0642.","offset":100,"name":"Qatari Rials"},"RON":{"iso":"RON","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Romanian Leu"},"RUB":{"iso":"RUB","format":"{symbol}{amount}","symbol":"p.","offset":100,"name":"Russian Ruble"},"SAR":{"iso":"SAR","format":"{symbol}{amount}","symbol":"\u0631.\u0633.","offset":100,"name":"Saudi Arabian Riyal"},"SEK":{"iso":"SEK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Swedish Krona"},"SGD":{"iso":"SGD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Singapore Dollar"},"THB":{"iso":"THB","format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"name":"Thai Baht"},"TRY":{"iso":"TRY","format":"{symbol}{amount}","symbol":"TL","offset":100,"name":"Turkish Lira"},"TWD":{"iso":"TWD","format":"{symbol}{amount}","symbol":"NT$","offset":1,"name":"Taiwan Dollar"},"UAH":{"iso":"UAH","format":"{symbol}{amount}","symbol":"\u20b4","offset":100,"name":"Ukrainian Hryvnia"},"USD":{"iso":"USD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"US Dollars"},"UYU":{"iso":"UYU","format":"{symbol}{amount}","symbol":"$U","offset":100,"name":"Uruguayan Peso"},"VND":{"iso":"VND","format":"{symbol}{amount}","symbol":"\u20ab","offset":1,"name":"Vietnamese Dong"},"ZAR":{"iso":"ZAR","format":"{symbol}{amount}","symbol":"R","offset":100,"name":"South African Rand"}},"adsCurrencyCodes":["AED","ARS","AUD","BDT","BOB","BRL","CAD","CHF","CLP","CNY","COP","CRC","CZK","DKK","DZD","EGP","EUR","GBP","GTQ","HKD","HNL","HUF","IDR","ILS","INR","ISK","JPY","KES","KRW","MOP","MXN","MYR","NGN","NIO","NOK","NZD","PEN","PHP","PKR","PLN","PYG","QAR","RON","RUB","SAR","SEK","SGD","THB","TRY","TWD","UAH","USD","UYU","VND","ZAR"],"adsBackendCurrencyByCode":{"AED":{"iso":"AED","format":"{symbol}{amount}","symbol":"\u062f.\u0625","offset":100,"name":"UAE Dirham"},"ARS":{"iso":"ARS","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Argentine Peso"},"AUD":{"iso":"AUD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Australian Dollar"},"BDT":{"iso":"BDT","format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"name":"Bangladeshi Taka"},"BOB":{"iso":"BOB","format":"{symbol}{amount}","symbol":"Bs.","offset":100,"name":"Bolivian Boliviano"},"BRL":{"iso":"BRL","format":"{symbol}{amount}","symbol":"R$","offset":100,"name":"Brazilian Real"},"CAD":{"iso":"CAD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Canadian Dollar"},"CHF":{"iso":"CHF","format":"{symbol}{amount}","symbol":"Fr.","offset":100,"name":"Swiss Franc"},"CLP":{"iso":"CLP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Chilean Peso"},"CNY":{"iso":"CNY","format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"name":"Chinese Yuan"},"COP":{"iso":"COP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Colombian Peso"},"CRC":{"iso":"CRC","format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"name":"Costa Rican Col\u00f3n"},"CZK":{"iso":"CZK","format":"{symbol}{amount}","symbol":"K\u010d","offset":100,"name":"Czech Koruna"},"DKK":{"iso":"DKK","format":"{symbol}{amount}","symbol":"kr.","offset":100,"name":"Danish Krone"},"DZD":{"iso":"DZD","format":"{symbol}{amount}","symbol":"DA","offset":100,"name":"Algerian Dinar"},"EGP":{"iso":"EGP","format":"{symbol}{amount}","symbol":"\u062c.\u0645.","offset":100,"name":"Egyptian Pound"},"EUR":{"iso":"EUR","format":"{symbol}{amount}","symbol":"\u20ac","offset":100,"name":"Euro"},"GBP":{"iso":"GBP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"British Pound"},"GTQ":{"iso":"GTQ","format":"{symbol}{amount}","symbol":"Q","offset":100,"name":"Guatemalan Quetzal"},"HKD":{"iso":"HKD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Hong Kong Dollar"},"HNL":{"iso":"HNL","format":"{symbol}{amount}","symbol":"L.","offset":100,"name":"Honduran Lempira"},"HUF":{"iso":"HUF","format":"{symbol}{amount}","symbol":"Ft","offset":1,"name":"Hungarian Forint"},"IDR":{"iso":"IDR","format":"{symbol}{amount}","symbol":"Rp","offset":1,"name":"Indonesian Rupiah"},"ILS":{"iso":"ILS","format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"name":"Israeli New Shekel"},"INR":{"iso":"INR","format":"{symbol}{amount}","symbol":"\u20b9","offset":100,"name":"Indian Rupee"},"ISK":{"iso":"ISK","format":"{symbol}{amount}","symbol":"kr.","offset":1,"name":"Iceland Krona"},"JPY":{"iso":"JPY","format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"name":"Japanese Yen"},"KES":{"iso":"KES","format":"{symbol}{amount}","symbol":"KSh","offset":100,"name":"Kenyan Shilling"},"KRW":{"iso":"KRW","format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"name":"Korean Won"},"LKR":{"iso":"LKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Sri Lankan Rupee"},"MOP":{"iso":"MOP","format":"{symbol}{amount}","symbol":"MOP","offset":100,"name":"Macau Patacas"},"MXN":{"iso":"MXN","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Mexican Peso"},"MYR":{"iso":"MYR","format":"{symbol}{amount}","symbol":"RM","offset":100,"name":"Malaysian Ringgit"},"NGN":{"iso":"NGN","format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"name":"Nigerian Naira"},"NIO":{"iso":"NIO","format":"{symbol}{amount}","symbol":"C$","offset":100,"name":"Nicaraguan Cordoba"},"NOK":{"iso":"NOK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Norwegian Krone"},"NZD":{"iso":"NZD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"New Zealand Dollar"},"PEN":{"iso":"PEN","format":"{symbol}{amount}","symbol":"S\/","offset":100,"name":"Peruvian Nuevo Sol"},"PHP":{"iso":"PHP","format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"name":"Philippine Peso"},"PKR":{"iso":"PKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Pakistani Rupee"},"PLN":{"iso":"PLN","format":"{symbol}{amount}","symbol":"z\u0142","offset":100,"name":"Polish Zloty"},"PYG":{"iso":"PYG","format":"{symbol}{amount}","symbol":"\u20b2","offset":1,"name":"Paraguayan Guarani"},"QAR":{"iso":"QAR","format":"{symbol}{amount}","symbol":"\u0631.\u0642.","offset":100,"name":"Qatari Rials"},"RON":{"iso":"RON","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Romanian Leu"},"SAR":{"iso":"SAR","format":"{symbol}{amount}","symbol":"\u0631.\u0633.","offset":100,"name":"Saudi Arabian Riyal"},"SEK":{"iso":"SEK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Swedish Krona"},"SGD":{"iso":"SGD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Singapore Dollar"},"THB":{"iso":"THB","format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"name":"Thai Baht"},"TRY":{"iso":"TRY","format":"{symbol}{amount}","symbol":"TL","offset":100,"name":"Turkish Lira"},"TWD":{"iso":"TWD","format":"{symbol}{amount}","symbol":"NT$","offset":1,"name":"Taiwan Dollar"},"UAH":{"iso":"UAH","format":"{symbol}{amount}","symbol":"\u20b4","offset":100,"name":"Ukrainian Hryvnia"},"USD":{"iso":"USD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"US Dollars"},"UYU":{"iso":"UYU","format":"{symbol}{amount}","symbol":"$U","offset":100,"name":"Uruguayan Peso"},"VND":{"iso":"VND","format":"{symbol}{amount}","symbol":"\u20ab","offset":1,"name":"Vietnamese Dong"},"ZAR":{"iso":"ZAR","format":"{symbol}{amount}","symbol":"R","offset":100,"name":"South African Rand"}},"adsBackendCurrencyCodes":["AED","ARS","AUD","BDT","BOB","BRL","CAD","CHF","CLP","CNY","COP","CRC","CZK","DKK","DZD","EGP","EUR","GBP","GTQ","HKD","HNL","HUF","IDR","ILS","INR","ISK","JPY","KES","KRW","LKR","MOP","MXN","MYR","NGN","NIO","NOK","NZD","PEN","PHP","PKR","PLN","PYG","QAR","RON","SAR","SEK","SGD","THB","TRY","TWD","UAH","USD","UYU","VND","ZAR"],"allCurrenciesByCode":{"AED":{"iso":"AED","format":"{symbol}{amount}","symbol":"\u062f.\u0625","offset":100,"name":"UAE Dirham"},"AFN":{"iso":"AFN","format":"{symbol}{amount}","symbol":"\u060b","offset":100,"name":"Afghan Afghani"},"ALL":{"iso":"ALL","format":"{symbol}{amount}","symbol":"Lek","offset":100,"name":"Albanian Lek"},"AMD":{"iso":"AMD","format":"{symbol}{amount}","symbol":"\u0564\u0580.","offset":100,"name":"Armenian Dram"},"ANG":{"iso":"ANG","format":"{symbol}{amount}","symbol":"\u0192","offset":100,"name":"Netherlands Antillean Guilder"},"AOA":{"iso":"AOA","format":"{symbol}{amount}","symbol":"Kz","offset":100,"name":"Angolan Kwanza"},"ARS":{"iso":"ARS","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Argentine Peso"},"AUD":{"iso":"AUD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Australian Dollar"},"AWG":{"iso":"AWG","format":"{symbol}{amount}","symbol":"Afl.","offset":100,"name":"Aruban Florin"},"AZN":{"iso":"AZN","format":"{symbol}{amount}","symbol":"\u043c\u0430\u043d.","offset":100,"name":"Azerbaijani Manat"},"BAM":{"iso":"BAM","format":"{symbol}{amount}","symbol":"KM","offset":100,"name":"Bosnian Herzegovinian Convertible Mark"},"BBD":{"iso":"BBD","format":"{symbol}{amount}","symbol":"Bds$","offset":100,"name":"Barbados Dollar"},"BDT":{"iso":"BDT","format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"name":"Bangladeshi Taka"},"BGN":{"iso":"BGN","format":"{symbol}{amount}","symbol":"\u043b\u0432.","offset":100,"name":"Bulgarian Lev"},"BHD":{"iso":"BHD","format":"{symbol}{amount}","symbol":"\u062f.\u0628.","offset":100,"name":"Bahraini Dinar"},"BIF":{"iso":"BIF","format":"{symbol}{amount}","symbol":"FBu","offset":1,"name":"Burundian Franc"},"BMD":{"iso":"BMD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Bermudian Dollar"},"BND":{"iso":"BND","format":"{symbol}{amount}","symbol":"B$","offset":100,"name":"Brunei Dollar"},"BOB":{"iso":"BOB","format":"{symbol}{amount}","symbol":"Bs.","offset":100,"name":"Bolivian Boliviano"},"BRL":{"iso":"BRL","format":"{symbol}{amount}","symbol":"R$","offset":100,"name":"Brazilian Real"},"BSD":{"iso":"BSD","format":"{symbol}{amount}","symbol":"B$","offset":100,"name":"Bahamian Dollar"},"BTN":{"iso":"BTN","format":"{symbol}{amount}","symbol":"Nu.","offset":100,"name":"Bhutanese Ngultrum"},"BWP":{"iso":"BWP","format":"{symbol}{amount}","symbol":"P","offset":100,"name":"Botswanan Pula"},"BYN":{"iso":"BYN","format":"{symbol}{amount}","symbol":"Br","offset":100,"name":"Belarusian Ruble"},"BZD":{"iso":"BZD","format":"{symbol}{amount}","symbol":"BZ$","offset":100,"name":"Belize Dollar"},"CAD":{"iso":"CAD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Canadian Dollar"},"CDF":{"iso":"CDF","format":"{symbol}{amount}","symbol":"FC","offset":100,"name":"Congolese Franc"},"CHF":{"iso":"CHF","format":"{symbol}{amount}","symbol":"Fr.","offset":100,"name":"Swiss Franc"},"CLF":{"iso":"CLF","format":"{symbol}{amount}","symbol":"E\u00ba","offset":100,"name":"Chilean Unit of Account"},"CLP":{"iso":"CLP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Chilean Peso"},"CNY":{"iso":"CNY","format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"name":"Chinese Yuan"},"COP":{"iso":"COP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Colombian Peso"},"CRC":{"iso":"CRC","format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"name":"Costa Rican Col\u00f3n"},"CVE":{"iso":"CVE","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Cape Verde Escudo"},"CZK":{"iso":"CZK","format":"{symbol}{amount}","symbol":"K\u010d","offset":100,"name":"Czech Koruna"},"DJF":{"iso":"DJF","format":"{symbol}{amount}","symbol":"Fdj","offset":1,"name":"Djiboutian Franc"},"DKK":{"iso":"DKK","format":"{symbol}{amount}","symbol":"kr.","offset":100,"name":"Danish Krone"},"DOP":{"iso":"DOP","format":"{symbol}{amount}","symbol":"RD$","offset":100,"name":"Dominican Peso"},"DZD":{"iso":"DZD","format":"{symbol}{amount}","symbol":"DA","offset":100,"name":"Algerian Dinar"},"EGP":{"iso":"EGP","format":"{symbol}{amount}","symbol":"\u062c.\u0645.","offset":100,"name":"Egyptian Pound"},"ERN":{"iso":"ERN","format":"{symbol}{amount}","symbol":"Nfk","offset":100,"name":"Eritrean Nakfa"},"ETB":{"iso":"ETB","format":"{symbol}{amount}","symbol":"Br","offset":100,"name":"Ethiopian Birr"},"EUR":{"iso":"EUR","format":"{symbol}{amount}","symbol":"\u20ac","offset":100,"name":"Euro"},"FBZ":{"iso":"FBZ","format":"{symbol}{amount}","symbol":"C","offset":100,"name":"credits"},"FJD":{"iso":"FJD","format":"{symbol}{amount}","symbol":"FJ$","offset":100,"name":"Fiji Dollar"},"FKP":{"iso":"FKP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Falkland Islands Pound"},"GBP":{"iso":"GBP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"British Pound"},"GEL":{"iso":"GEL","format":"{symbol}{amount}","symbol":"\u20be","offset":100,"name":"Georgian Lari"},"GHS":{"iso":"GHS","format":"{symbol}{amount}","symbol":"GHS","offset":100,"name":"Ghanaian Cedi"},"GIP":{"iso":"GIP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Gibraltar Pound"},"GMD":{"iso":"GMD","format":"{symbol}{amount}","symbol":"D","offset":100,"name":"Gambian Dalasi"},"GNF":{"iso":"GNF","format":"{symbol}{amount}","symbol":"FG","offset":1,"name":"Guinean Franc"},"GTQ":{"iso":"GTQ","format":"{symbol}{amount}","symbol":"Q","offset":100,"name":"Guatemalan Quetzal"},"GYD":{"iso":"GYD","format":"{symbol}{amount}","symbol":"G$","offset":100,"name":"Guyanese Dollar"},"HKD":{"iso":"HKD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Hong Kong Dollar"},"HNL":{"iso":"HNL","format":"{symbol}{amount}","symbol":"L.","offset":100,"name":"Honduran Lempira"},"HRK":{"iso":"HRK","format":"{symbol}{amount}","symbol":"kn","offset":100,"name":"Croatian Kuna"},"HTG":{"iso":"HTG","format":"{symbol}{amount}","symbol":"G","offset":100,"name":"Haitian Gourde"},"HUF":{"iso":"HUF","format":"{symbol}{amount}","symbol":"Ft","offset":1,"name":"Hungarian Forint"},"IDR":{"iso":"IDR","format":"{symbol}{amount}","symbol":"Rp","offset":1,"name":"Indonesian Rupiah"},"ILS":{"iso":"ILS","format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"name":"Israeli New Shekel"},"INR":{"iso":"INR","format":"{symbol}{amount}","symbol":"\u20b9","offset":100,"name":"Indian Rupee"},"IQD":{"iso":"IQD","format":"{symbol}{amount}","symbol":"\u062f.\u0639.","offset":100,"name":"Iraqi Dinar"},"ISK":{"iso":"ISK","format":"{symbol}{amount}","symbol":"kr.","offset":1,"name":"Iceland Krona"},"JMD":{"iso":"JMD","format":"{symbol}{amount}","symbol":"J$","offset":100,"name":"Jamaican Dollar"},"JOD":{"iso":"JOD","format":"{symbol}{amount}","symbol":"\u062f.\u0627.","offset":100,"name":"Jordanian Dinar"},"JPY":{"iso":"JPY","format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"name":"Japanese Yen"},"KES":{"iso":"KES","format":"{symbol}{amount}","symbol":"KSh","offset":100,"name":"Kenyan Shilling"},"KGS":{"iso":"KGS","format":"{symbol}{amount}","symbol":"\u0441\u043e\u043c","offset":100,"name":"Kyrgyzstani Som"},"KHR":{"iso":"KHR","format":"{symbol}{amount}","symbol":"\u17db","offset":100,"name":"Cambodian Riel"},"KMF":{"iso":"KMF","format":"{symbol}{amount}","symbol":"CF","offset":1,"name":"Comoro Franc"},"KRW":{"iso":"KRW","format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"name":"Korean Won"},"KWD":{"iso":"KWD","format":"{symbol}{amount}","symbol":"\u062f.\u0643.","offset":100,"name":"Kuwaiti Dinar"},"KYD":{"iso":"KYD","format":"{symbol}{amount}","symbol":"CI$","offset":100,"name":"Cayman Islands Dollar"},"KZT":{"iso":"KZT","format":"{symbol}{amount}","symbol":"\u0422","offset":100,"name":"Kazakhstani Tenge"},"LAK":{"iso":"LAK","format":"{symbol}{amount}","symbol":"\u20ad","offset":100,"name":"Lao Kip"},"LBP":{"iso":"LBP","format":"{symbol}{amount}","symbol":"\u0644.\u0644.","offset":100,"name":"Lebanese Pound"},"LKR":{"iso":"LKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Sri Lankan Rupee"},"LRD":{"iso":"LRD","format":"{symbol}{amount}","symbol":"L$","offset":100,"name":"Liberian Dollar"},"LSL":{"iso":"LSL","format":"{symbol}{amount}","symbol":"M","offset":100,"name":"Lesotho Loti"},"LTL":{"iso":"LTL","format":"{symbol}{amount}","symbol":"Lt","offset":100,"name":"Lithuanian Litas"},"LVL":{"iso":"LVL","format":"{symbol}{amount}","symbol":"Ls","offset":100,"name":"Latvian Lats"},"LYD":{"iso":"LYD","format":"{symbol}{amount}","symbol":"\u062f.\u0644.","offset":100,"name":"Libyan Dinar"},"MAD":{"iso":"MAD","format":"{symbol}{amount}","symbol":"\u062f.\u0645.","offset":100,"name":"Moroccan Dirham"},"MDL":{"iso":"MDL","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Moldovan Leu"},"MGA":{"iso":"MGA","format":"{symbol}{amount}","symbol":"Ar","offset":5,"name":"Malagasy Ariary"},"MKD":{"iso":"MKD","format":"{symbol}{amount}","symbol":"\u0434\u0435\u043d.","offset":100,"name":"Macedonian Denar"},"MMK":{"iso":"MMK","format":"{symbol}{amount}","symbol":"Ks","offset":100,"name":"Burmese Kyat"},"MNT":{"iso":"MNT","format":"{symbol}{amount}","symbol":"\u20ae","offset":100,"name":"Mongolian Tugrik"},"MOP":{"iso":"MOP","format":"{symbol}{amount}","symbol":"MOP","offset":100,"name":"Macau Patacas"},"MRO":{"iso":"MRO","format":"{symbol}{amount}","symbol":"UM","offset":5,"name":"Mauritanian Ouguiya"},"MUR":{"iso":"MUR","format":"{symbol}{amount}","symbol":"\u20a8","offset":100,"name":"Mauritian Rupee"},"MVR":{"iso":"MVR","format":"{symbol}{amount}","symbol":"\u0783.","offset":100,"name":"Maldivian Rufiyaa"},"MWK":{"iso":"MWK","format":"{symbol}{amount}","symbol":"MK","offset":100,"name":"Malawian Kwacha"},"MXN":{"iso":"MXN","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Mexican Peso"},"MYR":{"iso":"MYR","format":"{symbol}{amount}","symbol":"RM","offset":100,"name":"Malaysian Ringgit"},"MZN":{"iso":"MZN","format":"{symbol}{amount}","symbol":"MT","offset":100,"name":"Mozambican Metical"},"NAD":{"iso":"NAD","format":"{symbol}{amount}","symbol":"N$","offset":100,"name":"Namibian Dollar"},"NGN":{"iso":"NGN","format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"name":"Nigerian Naira"},"NIO":{"iso":"NIO","format":"{symbol}{amount}","symbol":"C$","offset":100,"name":"Nicaraguan Cordoba"},"NOK":{"iso":"NOK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Norwegian Krone"},"NPR":{"iso":"NPR","format":"{symbol}{amount}","symbol":"\u0930\u0942","offset":100,"name":"Nepalese Rupee"},"NZD":{"iso":"NZD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"New Zealand Dollar"},"OMR":{"iso":"OMR","format":"{symbol}{amount}","symbol":"\u0631.\u0639.","offset":100,"name":"Omani Rial"},"PAB":{"iso":"PAB","format":"{symbol}{amount}","symbol":"B\/.","offset":100,"name":"Panamanian Balboas"},"PEN":{"iso":"PEN","format":"{symbol}{amount}","symbol":"S\/","offset":100,"name":"Peruvian Nuevo Sol"},"PGK":{"iso":"PGK","format":"{symbol}{amount}","symbol":"K","offset":100,"name":"Papua New Guinean Kina"},"PHP":{"iso":"PHP","format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"name":"Philippine Peso"},"PKR":{"iso":"PKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Pakistani Rupee"},"PLN":{"iso":"PLN","format":"{symbol}{amount}","symbol":"z\u0142","offset":100,"name":"Polish Zloty"},"PYG":{"iso":"PYG","format":"{symbol}{amount}","symbol":"\u20b2","offset":1,"name":"Paraguayan Guarani"},"QAR":{"iso":"QAR","format":"{symbol}{amount}","symbol":"\u0631.\u0642.","offset":100,"name":"Qatari Rials"},"RON":{"iso":"RON","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Romanian Leu"},"RSD":{"iso":"RSD","format":"{symbol}{amount}","symbol":"din","offset":100,"name":"Serbian Dinar"},"RUB":{"iso":"RUB","format":"{symbol}{amount}","symbol":"p.","offset":100,"name":"Russian Ruble"},"RWF":{"iso":"RWF","format":"{symbol}{amount}","symbol":"FRw","offset":1,"name":"Rwandan Franc"},"SAR":{"iso":"SAR","format":"{symbol}{amount}","symbol":"\u0631.\u0633.","offset":100,"name":"Saudi Arabian Riyal"},"SBD":{"iso":"SBD","format":"{symbol}{amount}","symbol":"SI$","offset":100,"name":"Solomon Islands Dollar"},"SCR":{"iso":"SCR","format":"{symbol}{amount}","symbol":"SR","offset":100,"name":"Seychelles Rupee"},"SEK":{"iso":"SEK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Swedish Krona"},"SGD":{"iso":"SGD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Singapore Dollar"},"SHP":{"iso":"SHP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Saint Helena Pound"},"SKK":{"iso":"SKK","format":"{symbol}{amount}","symbol":"Sk","offset":100,"name":"Slovak Koruna"},"SLE":{"iso":"SLE","format":"{symbol}{amount}","symbol":"Le","offset":100,"name":"Sierra Leonean Leone"},"SLL":{"iso":"SLL","format":"{symbol}{amount}","symbol":"Le","offset":100,"name":"Sierra Leonean Old Leone"},"SOS":{"iso":"SOS","format":"{symbol}{amount}","symbol":"S","offset":100,"name":"Somali Shilling"},"SRD":{"iso":"SRD","format":"{symbol}{amount}","symbol":"SRD","offset":100,"name":"Surinamese Dollar"},"SSP":{"iso":"SSP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"South Sudanese Pound"},"STD":{"iso":"STD","format":"{symbol}{amount}","symbol":"Db","offset":100,"name":"Sao Tome and Principe Dobra"},"SVC":{"iso":"SVC","format":"{symbol}{amount}","symbol":"\u20a1","offset":100,"name":"Salvadoran Col\u00f3n"},"SZL":{"iso":"SZL","format":"{symbol}{amount}","symbol":"L","offset":100,"name":"Swazi Lilangeni"},"THB":{"iso":"THB","format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"name":"Thai Baht"},"TJS":{"iso":"TJS","format":"{symbol}{amount}","symbol":"\u0441.","offset":100,"name":"Tajikistani Somoni"},"TMT":{"iso":"TMT","format":"{symbol}{amount}","symbol":"T","offset":100,"name":"Turkmenistani Manat"},"TND":{"iso":"TND","format":"{symbol}{amount}","symbol":"\u062f.\u062a.","offset":100,"name":"Tunisian Dinar"},"TOP":{"iso":"TOP","format":"{symbol}{amount}","symbol":"T$","offset":100,"name":"Tongan Pa\u02bbanga"},"TRY":{"iso":"TRY","format":"{symbol}{amount}","symbol":"TL","offset":100,"name":"Turkish Lira"},"TTD":{"iso":"TTD","format":"{symbol}{amount}","symbol":"TT$","offset":100,"name":"Trinidad and Tobago Dollar"},"TWD":{"iso":"TWD","format":"{symbol}{amount}","symbol":"NT$","offset":1,"name":"Taiwan Dollar"},"TZS":{"iso":"TZS","format":"{symbol}{amount}","symbol":"TSh","offset":100,"name":"Tanzanian Shilling"},"UAH":{"iso":"UAH","format":"{symbol}{amount}","symbol":"\u20b4","offset":100,"name":"Ukrainian Hryvnia"},"UGX":{"iso":"UGX","format":"{symbol}{amount}","symbol":"USh","offset":1,"name":"Ugandan Shilling"},"USD":{"iso":"USD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"US Dollars"},"UYU":{"iso":"UYU","format":"{symbol}{amount}","symbol":"$U","offset":100,"name":"Uruguayan Peso"},"UZS":{"iso":"UZS","format":"{symbol}{amount}","symbol":"\u0441\u045e\u043c","offset":100,"name":"Uzbekistan Som"},"VEF":{"iso":"VEF","format":"{symbol}{amount}","symbol":"Bs.F","offset":100,"name":"Venezuelan Bolivar"},"VES":{"iso":"VES","format":"{symbol}{amount}","symbol":"Bs.S","offset":100,"name":"Venezuelan Sovereign Bolivar"},"VND":{"iso":"VND","format":"{symbol}{amount}","symbol":"\u20ab","offset":1,"name":"Vietnamese Dong"},"VUV":{"iso":"VUV","format":"{symbol}{amount}","symbol":"VT","offset":1,"name":"Vanuatu Vatu"},"WST":{"iso":"WST","format":"{symbol}{amount}","symbol":"WS$","offset":100,"name":"Samoan Tala"},"XAF":{"iso":"XAF","format":"{symbol}{amount}","symbol":"FCFA","offset":1,"name":"Central African Frank"},"XCD":{"iso":"XCD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"East Caribbean Dollar"},"XOF":{"iso":"XOF","format":"{symbol}{amount}","symbol":"FCFA","offset":1,"name":"West African Frank"},"XPF":{"iso":"XPF","format":"{symbol}{amount}","symbol":"\u20a3","offset":1,"name":"CFP Franc"},"YER":{"iso":"YER","format":"{symbol}{amount}","symbol":"\u0631.\u064a.","offset":100,"name":"Yemeni Rial"},"ZAR":{"iso":"ZAR","format":"{symbol}{amount}","symbol":"R","offset":100,"name":"South African Rand"},"ZMW":{"iso":"ZMW","format":"{symbol}{amount}","symbol":"K","offset":100,"name":"Zambian Kwacha"},"ZWL":{"iso":"ZWL","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Zimbabwean Dollar"}},"dynamicAdsCurrenciesByCode":{"AED":{"iso":"AED","format":"{symbol}{amount}","symbol":"\u062f.\u0625","offset":100,"name":"UAE Dirham"},"AFN":{"iso":"AFN","format":"{symbol}{amount}","symbol":"\u060b","offset":100,"name":"Afghan Afghani"},"ALL":{"iso":"ALL","format":"{symbol}{amount}","symbol":"Lek","offset":100,"name":"Albanian Lek"},"AMD":{"iso":"AMD","format":"{symbol}{amount}","symbol":"\u0564\u0580.","offset":100,"name":"Armenian Dram"},"ANG":{"iso":"ANG","format":"{symbol}{amount}","symbol":"\u0192","offset":100,"name":"Netherlands Antillean Guilder"},"AOA":{"iso":"AOA","format":"{symbol}{amount}","symbol":"Kz","offset":100,"name":"Angolan Kwanza"},"ARS":{"iso":"ARS","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Argentine Peso"},"AUD":{"iso":"AUD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Australian Dollar"},"AWG":{"iso":"AWG","format":"{symbol}{amount}","symbol":"Afl.","offset":100,"name":"Aruban Florin"},"AZN":{"iso":"AZN","format":"{symbol}{amount}","symbol":"\u043c\u0430\u043d.","offset":100,"name":"Azerbaijani Manat"},"BAM":{"iso":"BAM","format":"{symbol}{amount}","symbol":"KM","offset":100,"name":"Bosnian Herzegovinian Convertible Mark"},"BBD":{"iso":"BBD","format":"{symbol}{amount}","symbol":"Bds$","offset":100,"name":"Barbados Dollar"},"BDT":{"iso":"BDT","format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"name":"Bangladeshi Taka"},"BGN":{"iso":"BGN","format":"{symbol}{amount}","symbol":"\u043b\u0432.","offset":100,"name":"Bulgarian Lev"},"BHD":{"iso":"BHD","format":"{symbol}{amount}","symbol":"\u062f.\u0628.","offset":100,"name":"Bahraini Dinar"},"BIF":{"iso":"BIF","format":"{symbol}{amount}","symbol":"FBu","offset":1,"name":"Burundian Franc"},"BMD":{"iso":"BMD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Bermudian Dollar"},"BND":{"iso":"BND","format":"{symbol}{amount}","symbol":"B$","offset":100,"name":"Brunei Dollar"},"BOB":{"iso":"BOB","format":"{symbol}{amount}","symbol":"Bs.","offset":100,"name":"Bolivian Boliviano"},"BRL":{"iso":"BRL","format":"{symbol}{amount}","symbol":"R$","offset":100,"name":"Brazilian Real"},"BSD":{"iso":"BSD","format":"{symbol}{amount}","symbol":"B$","offset":100,"name":"Bahamian Dollar"},"BTN":{"iso":"BTN","format":"{symbol}{amount}","symbol":"Nu.","offset":100,"name":"Bhutanese Ngultrum"},"BWP":{"iso":"BWP","format":"{symbol}{amount}","symbol":"P","offset":100,"name":"Botswanan Pula"},"BYN":{"iso":"BYN","format":"{symbol}{amount}","symbol":"Br","offset":100,"name":"Belarusian Ruble"},"BZD":{"iso":"BZD","format":"{symbol}{amount}","symbol":"BZ$","offset":100,"name":"Belize Dollar"},"CAD":{"iso":"CAD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Canadian Dollar"},"CDF":{"iso":"CDF","format":"{symbol}{amount}","symbol":"FC","offset":100,"name":"Congolese Franc"},"CHF":{"iso":"CHF","format":"{symbol}{amount}","symbol":"Fr.","offset":100,"name":"Swiss Franc"},"CLF":{"iso":"CLF","format":"{symbol}{amount}","symbol":"E\u00ba","offset":100,"name":"Chilean Unit of Account"},"CLP":{"iso":"CLP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Chilean Peso"},"CNY":{"iso":"CNY","format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"name":"Chinese Yuan"},"COP":{"iso":"COP","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Colombian Peso"},"CRC":{"iso":"CRC","format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"name":"Costa Rican Col\u00f3n"},"CVE":{"iso":"CVE","format":"{symbol}{amount}","symbol":"$","offset":1,"name":"Cape Verde Escudo"},"CZK":{"iso":"CZK","format":"{symbol}{amount}","symbol":"K\u010d","offset":100,"name":"Czech Koruna"},"DJF":{"iso":"DJF","format":"{symbol}{amount}","symbol":"Fdj","offset":1,"name":"Djiboutian Franc"},"DKK":{"iso":"DKK","format":"{symbol}{amount}","symbol":"kr.","offset":100,"name":"Danish Krone"},"DOP":{"iso":"DOP","format":"{symbol}{amount}","symbol":"RD$","offset":100,"name":"Dominican Peso"},"DZD":{"iso":"DZD","format":"{symbol}{amount}","symbol":"DA","offset":100,"name":"Algerian Dinar"},"EGP":{"iso":"EGP","format":"{symbol}{amount}","symbol":"\u062c.\u0645.","offset":100,"name":"Egyptian Pound"},"ERN":{"iso":"ERN","format":"{symbol}{amount}","symbol":"Nfk","offset":100,"name":"Eritrean Nakfa"},"ETB":{"iso":"ETB","format":"{symbol}{amount}","symbol":"Br","offset":100,"name":"Ethiopian Birr"},"EUR":{"iso":"EUR","format":"{symbol}{amount}","symbol":"\u20ac","offset":100,"name":"Euro"},"FJD":{"iso":"FJD","format":"{symbol}{amount}","symbol":"FJ$","offset":100,"name":"Fiji Dollar"},"FKP":{"iso":"FKP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Falkland Islands Pound"},"GBP":{"iso":"GBP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"British Pound"},"GEL":{"iso":"GEL","format":"{symbol}{amount}","symbol":"\u20be","offset":100,"name":"Georgian Lari"},"GHS":{"iso":"GHS","format":"{symbol}{amount}","symbol":"GHS","offset":100,"name":"Ghanaian Cedi"},"GIP":{"iso":"GIP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Gibraltar Pound"},"GMD":{"iso":"GMD","format":"{symbol}{amount}","symbol":"D","offset":100,"name":"Gambian Dalasi"},"GNF":{"iso":"GNF","format":"{symbol}{amount}","symbol":"FG","offset":1,"name":"Guinean Franc"},"GTQ":{"iso":"GTQ","format":"{symbol}{amount}","symbol":"Q","offset":100,"name":"Guatemalan Quetzal"},"GYD":{"iso":"GYD","format":"{symbol}{amount}","symbol":"G$","offset":100,"name":"Guyanese Dollar"},"HKD":{"iso":"HKD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Hong Kong Dollar"},"HNL":{"iso":"HNL","format":"{symbol}{amount}","symbol":"L.","offset":100,"name":"Honduran Lempira"},"HRK":{"iso":"HRK","format":"{symbol}{amount}","symbol":"kn","offset":100,"name":"Croatian Kuna"},"HTG":{"iso":"HTG","format":"{symbol}{amount}","symbol":"G","offset":100,"name":"Haitian Gourde"},"HUF":{"iso":"HUF","format":"{symbol}{amount}","symbol":"Ft","offset":1,"name":"Hungarian Forint"},"IDR":{"iso":"IDR","format":"{symbol}{amount}","symbol":"Rp","offset":1,"name":"Indonesian Rupiah"},"ILS":{"iso":"ILS","format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"name":"Israeli New Shekel"},"INR":{"iso":"INR","format":"{symbol}{amount}","symbol":"\u20b9","offset":100,"name":"Indian Rupee"},"IQD":{"iso":"IQD","format":"{symbol}{amount}","symbol":"\u062f.\u0639.","offset":100,"name":"Iraqi Dinar"},"ISK":{"iso":"ISK","format":"{symbol}{amount}","symbol":"kr.","offset":1,"name":"Iceland Krona"},"JMD":{"iso":"JMD","format":"{symbol}{amount}","symbol":"J$","offset":100,"name":"Jamaican Dollar"},"JOD":{"iso":"JOD","format":"{symbol}{amount}","symbol":"\u062f.\u0627.","offset":100,"name":"Jordanian Dinar"},"JPY":{"iso":"JPY","format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"name":"Japanese Yen"},"KES":{"iso":"KES","format":"{symbol}{amount}","symbol":"KSh","offset":100,"name":"Kenyan Shilling"},"KGS":{"iso":"KGS","format":"{symbol}{amount}","symbol":"\u0441\u043e\u043c","offset":100,"name":"Kyrgyzstani Som"},"KHR":{"iso":"KHR","format":"{symbol}{amount}","symbol":"\u17db","offset":100,"name":"Cambodian Riel"},"KMF":{"iso":"KMF","format":"{symbol}{amount}","symbol":"CF","offset":1,"name":"Comoro Franc"},"KRW":{"iso":"KRW","format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"name":"Korean Won"},"KWD":{"iso":"KWD","format":"{symbol}{amount}","symbol":"\u062f.\u0643.","offset":100,"name":"Kuwaiti Dinar"},"KYD":{"iso":"KYD","format":"{symbol}{amount}","symbol":"CI$","offset":100,"name":"Cayman Islands Dollar"},"KZT":{"iso":"KZT","format":"{symbol}{amount}","symbol":"\u0422","offset":100,"name":"Kazakhstani Tenge"},"LAK":{"iso":"LAK","format":"{symbol}{amount}","symbol":"\u20ad","offset":100,"name":"Lao Kip"},"LBP":{"iso":"LBP","format":"{symbol}{amount}","symbol":"\u0644.\u0644.","offset":100,"name":"Lebanese Pound"},"LKR":{"iso":"LKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Sri Lankan Rupee"},"LRD":{"iso":"LRD","format":"{symbol}{amount}","symbol":"L$","offset":100,"name":"Liberian Dollar"},"LSL":{"iso":"LSL","format":"{symbol}{amount}","symbol":"M","offset":100,"name":"Lesotho Loti"},"LYD":{"iso":"LYD","format":"{symbol}{amount}","symbol":"\u062f.\u0644.","offset":100,"name":"Libyan Dinar"},"MAD":{"iso":"MAD","format":"{symbol}{amount}","symbol":"\u062f.\u0645.","offset":100,"name":"Moroccan Dirham"},"MDL":{"iso":"MDL","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Moldovan Leu"},"MGA":{"iso":"MGA","format":"{symbol}{amount}","symbol":"Ar","offset":5,"name":"Malagasy Ariary"},"MKD":{"iso":"MKD","format":"{symbol}{amount}","symbol":"\u0434\u0435\u043d.","offset":100,"name":"Macedonian Denar"},"MMK":{"iso":"MMK","format":"{symbol}{amount}","symbol":"Ks","offset":100,"name":"Burmese Kyat"},"MNT":{"iso":"MNT","format":"{symbol}{amount}","symbol":"\u20ae","offset":100,"name":"Mongolian Tugrik"},"MOP":{"iso":"MOP","format":"{symbol}{amount}","symbol":"MOP","offset":100,"name":"Macau Patacas"},"MRO":{"iso":"MRO","format":"{symbol}{amount}","symbol":"UM","offset":5,"name":"Mauritanian Ouguiya"},"MUR":{"iso":"MUR","format":"{symbol}{amount}","symbol":"\u20a8","offset":100,"name":"Mauritian Rupee"},"MVR":{"iso":"MVR","format":"{symbol}{amount}","symbol":"\u0783.","offset":100,"name":"Maldivian Rufiyaa"},"MWK":{"iso":"MWK","format":"{symbol}{amount}","symbol":"MK","offset":100,"name":"Malawian Kwacha"},"MXN":{"iso":"MXN","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Mexican Peso"},"MYR":{"iso":"MYR","format":"{symbol}{amount}","symbol":"RM","offset":100,"name":"Malaysian Ringgit"},"MZN":{"iso":"MZN","format":"{symbol}{amount}","symbol":"MT","offset":100,"name":"Mozambican Metical"},"NAD":{"iso":"NAD","format":"{symbol}{amount}","symbol":"N$","offset":100,"name":"Namibian Dollar"},"NGN":{"iso":"NGN","format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"name":"Nigerian Naira"},"NIO":{"iso":"NIO","format":"{symbol}{amount}","symbol":"C$","offset":100,"name":"Nicaraguan Cordoba"},"NOK":{"iso":"NOK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Norwegian Krone"},"NPR":{"iso":"NPR","format":"{symbol}{amount}","symbol":"\u0930\u0942","offset":100,"name":"Nepalese Rupee"},"NZD":{"iso":"NZD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"New Zealand Dollar"},"OMR":{"iso":"OMR","format":"{symbol}{amount}","symbol":"\u0631.\u0639.","offset":100,"name":"Omani Rial"},"PAB":{"iso":"PAB","format":"{symbol}{amount}","symbol":"B\/.","offset":100,"name":"Panamanian Balboas"},"PEN":{"iso":"PEN","format":"{symbol}{amount}","symbol":"S\/","offset":100,"name":"Peruvian Nuevo Sol"},"PGK":{"iso":"PGK","format":"{symbol}{amount}","symbol":"K","offset":100,"name":"Papua New Guinean Kina"},"PHP":{"iso":"PHP","format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"name":"Philippine Peso"},"PKR":{"iso":"PKR","format":"{symbol}{amount}","symbol":"Rs","offset":100,"name":"Pakistani Rupee"},"PLN":{"iso":"PLN","format":"{symbol}{amount}","symbol":"z\u0142","offset":100,"name":"Polish Zloty"},"PYG":{"iso":"PYG","format":"{symbol}{amount}","symbol":"\u20b2","offset":1,"name":"Paraguayan Guarani"},"QAR":{"iso":"QAR","format":"{symbol}{amount}","symbol":"\u0631.\u0642.","offset":100,"name":"Qatari Rials"},"RON":{"iso":"RON","format":"{symbol}{amount}","symbol":"lei","offset":100,"name":"Romanian Leu"},"RSD":{"iso":"RSD","format":"{symbol}{amount}","symbol":"din","offset":100,"name":"Serbian Dinar"},"RUB":{"iso":"RUB","format":"{symbol}{amount}","symbol":"p.","offset":100,"name":"Russian Ruble"},"RWF":{"iso":"RWF","format":"{symbol}{amount}","symbol":"FRw","offset":1,"name":"Rwandan Franc"},"SAR":{"iso":"SAR","format":"{symbol}{amount}","symbol":"\u0631.\u0633.","offset":100,"name":"Saudi Arabian Riyal"},"SBD":{"iso":"SBD","format":"{symbol}{amount}","symbol":"SI$","offset":100,"name":"Solomon Islands Dollar"},"SCR":{"iso":"SCR","format":"{symbol}{amount}","symbol":"SR","offset":100,"name":"Seychelles Rupee"},"SEK":{"iso":"SEK","format":"{symbol}{amount}","symbol":"kr","offset":100,"name":"Swedish Krona"},"SGD":{"iso":"SGD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Singapore Dollar"},"SHP":{"iso":"SHP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"Saint Helena Pound"},"SLL":{"iso":"SLL","format":"{symbol}{amount}","symbol":"Le","offset":100,"name":"Sierra Leonean Old Leone"},"SOS":{"iso":"SOS","format":"{symbol}{amount}","symbol":"S","offset":100,"name":"Somali Shilling"},"SRD":{"iso":"SRD","format":"{symbol}{amount}","symbol":"SRD","offset":100,"name":"Surinamese Dollar"},"SSP":{"iso":"SSP","format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"name":"South Sudanese Pound"},"STD":{"iso":"STD","format":"{symbol}{amount}","symbol":"Db","offset":100,"name":"Sao Tome and Principe Dobra"},"SVC":{"iso":"SVC","format":"{symbol}{amount}","symbol":"\u20a1","offset":100,"name":"Salvadoran Col\u00f3n"},"SZL":{"iso":"SZL","format":"{symbol}{amount}","symbol":"L","offset":100,"name":"Swazi Lilangeni"},"THB":{"iso":"THB","format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"name":"Thai Baht"},"TJS":{"iso":"TJS","format":"{symbol}{amount}","symbol":"\u0441.","offset":100,"name":"Tajikistani Somoni"},"TMT":{"iso":"TMT","format":"{symbol}{amount}","symbol":"T","offset":100,"name":"Turkmenistani Manat"},"TND":{"iso":"TND","format":"{symbol}{amount}","symbol":"\u062f.\u062a.","offset":100,"name":"Tunisian Dinar"},"TOP":{"iso":"TOP","format":"{symbol}{amount}","symbol":"T$","offset":100,"name":"Tongan Pa\u02bbanga"},"TRY":{"iso":"TRY","format":"{symbol}{amount}","symbol":"TL","offset":100,"name":"Turkish Lira"},"TTD":{"iso":"TTD","format":"{symbol}{amount}","symbol":"TT$","offset":100,"name":"Trinidad and Tobago Dollar"},"TWD":{"iso":"TWD","format":"{symbol}{amount}","symbol":"NT$","offset":1,"name":"Taiwan Dollar"},"TZS":{"iso":"TZS","format":"{symbol}{amount}","symbol":"TSh","offset":100,"name":"Tanzanian Shilling"},"UAH":{"iso":"UAH","format":"{symbol}{amount}","symbol":"\u20b4","offset":100,"name":"Ukrainian Hryvnia"},"UGX":{"iso":"UGX","format":"{symbol}{amount}","symbol":"USh","offset":1,"name":"Ugandan Shilling"},"USD":{"iso":"USD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"US Dollars"},"UYU":{"iso":"UYU","format":"{symbol}{amount}","symbol":"$U","offset":100,"name":"Uruguayan Peso"},"UZS":{"iso":"UZS","format":"{symbol}{amount}","symbol":"\u0441\u045e\u043c","offset":100,"name":"Uzbekistan Som"},"VEF":{"iso":"VEF","format":"{symbol}{amount}","symbol":"Bs.F","offset":100,"name":"Venezuelan Bolivar"},"VES":{"iso":"VES","format":"{symbol}{amount}","symbol":"Bs.S","offset":100,"name":"Venezuelan Sovereign Bolivar"},"VND":{"iso":"VND","format":"{symbol}{amount}","symbol":"\u20ab","offset":1,"name":"Vietnamese Dong"},"VUV":{"iso":"VUV","format":"{symbol}{amount}","symbol":"VT","offset":1,"name":"Vanuatu Vatu"},"WST":{"iso":"WST","format":"{symbol}{amount}","symbol":"WS$","offset":100,"name":"Samoan Tala"},"XAF":{"iso":"XAF","format":"{symbol}{amount}","symbol":"FCFA","offset":1,"name":"Central African Frank"},"XCD":{"iso":"XCD","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"East Caribbean Dollar"},"XOF":{"iso":"XOF","format":"{symbol}{amount}","symbol":"FCFA","offset":1,"name":"West African Frank"},"XPF":{"iso":"XPF","format":"{symbol}{amount}","symbol":"\u20a3","offset":1,"name":"CFP Franc"},"YER":{"iso":"YER","format":"{symbol}{amount}","symbol":"\u0631.\u064a.","offset":100,"name":"Yemeni Rial"},"ZAR":{"iso":"ZAR","format":"{symbol}{amount}","symbol":"R","offset":100,"name":"South African Rand"},"ZMW":{"iso":"ZMW","format":"{symbol}{amount}","symbol":"K","offset":100,"name":"Zambian Kwacha"},"ZWL":{"iso":"ZWL","format":"{symbol}{amount}","symbol":"$","offset":100,"name":"Zimbabwean Dollar"}}},832],["MRequestConfig",[],{"dtsg":{"token":"NAcNryKZUknKfEePxrmFURcjYctE_45ANRffjzlETa8gJ3Fu2I3Pthg:0:0","valid_for":86400,"expire":1742489383},"dtsg_ag":{"token":"AQwL4VF5NlNtlf_GS8yvgKfZEx5oFOM31MG1jnzs6E1bvBKK:0:0","valid_for":604800,"expire":1743007783},"lsd":"AVqzJ_kxCRU","checkResponseOrigin":true,"checkResponseToken":true,"cleanFinishedRequest":false,"cleanFinishedPrefetchRequests":false,"ajaxResponseToken":{"secret":"DhEgtuUMiu-_VL5yXXOeV9jSZ0H15XFo","encrypted":"AYm8R3k8cv2KZcoctst4lLCNxk0A8YOx_GjOmJWpoAu_c8XmeWYaigiA8WKYGY0WTOBbEgprKo7_ygQc_hoYl0iLwnGN-rUGAi8CmTtgCu_v0A"}},51],["PECurrencyConfig",[],{"currency_map_for_render":{"AED":{"format":"{amount} {symbol}","symbol":"\u062f.\u0625","offset":100,"screen_name":"UAE Dirham"},"AFN":{"format":"{symbol}{amount}","symbol":"\u060b","offset":100,"screen_name":"Afghan Afghani"},"ALL":{"format":"{amount}{symbol}","symbol":"Lek","offset":100,"screen_name":"Albanian Lek"},"AMD":{"format":"{amount} {symbol}","symbol":"\u0564\u0580.","offset":100,"screen_name":"Armenian Dram"},"ANG":{"format":"{symbol}{amount}","symbol":"\u0192","offset":100,"screen_name":"Netherlands Antillean Guilder"},"AOA":{"format":"{symbol}{amount}","symbol":"Kz","offset":100,"screen_name":"Angolan Kwanza"},"ARS":{"format":"{symbol} {amount}","symbol":"$","offset":100,"screen_name":"Argentine Peso"},"AUD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Australian Dollar"},"AWG":{"format":"{symbol}{amount}","symbol":"Afl.","offset":100,"screen_name":"Aruban Florin"},"AZN":{"format":"{amount} {symbol}","symbol":"\u043c\u0430\u043d.","offset":100,"screen_name":"Azerbaijani Manat"},"BAM":{"format":"{symbol}{amount}","symbol":"KM","offset":100,"screen_name":"Bosnian Herzegovinian Convertible Mark"},"BBD":{"format":"{symbol}{amount}","symbol":"Bds$","offset":100,"screen_name":"Barbados Dollar"},"BDT":{"format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"screen_name":"Bangladeshi Taka"},"BGN":{"format":"{amount} {symbol}","symbol":"\u043b\u0432.","offset":100,"screen_name":"Bulgarian Lev"},"BHD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0628.","offset":100,"screen_name":"Bahraini Dinar"},"BIF":{"format":"{symbol}{amount}","symbol":"FBu","offset":1,"screen_name":"Burundian Franc"},"BMD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Bermudian Dollar"},"BND":{"format":"{symbol}{amount}","symbol":"B$","offset":100,"screen_name":"Brunei Dollar"},"BOB":{"format":"{symbol} {amount}","symbol":"Bs.","offset":100,"screen_name":"Bolivian Boliviano"},"BRL":{"format":"{symbol} {amount}","symbol":"R$","offset":100,"screen_name":"Brazilian Real"},"BSD":{"format":"{symbol}{amount}","symbol":"B$","offset":100,"screen_name":"Bahamian Dollar"},"BTN":{"format":"{symbol}{amount}","symbol":"Nu.","offset":100,"screen_name":"Bhutanese Ngultrum"},"BWP":{"format":"{symbol}{amount}","symbol":"P","offset":100,"screen_name":"Botswanan Pula"},"BYN":{"format":"{amount} {symbol}","symbol":"Br","offset":100,"screen_name":"Belarusian Ruble"},"BZD":{"format":"{symbol}{amount}","symbol":"BZ$","offset":100,"screen_name":"Belize Dollar"},"CAD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Canadian Dollar"},"CDF":{"format":"{symbol}{amount}","symbol":"FC","offset":100,"screen_name":"Congolese Franc"},"CHF":{"format":"{symbol} {amount}","symbol":"Fr.","offset":100,"screen_name":"Swiss Franc"},"CLF":{"format":"{symbol}{amount}","symbol":"E\u00ba","offset":100,"screen_name":"Chilean Unit of Account"},"CLP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Chilean Peso"},"CNY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"screen_name":"Chinese Yuan"},"COP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Colombian Peso"},"CRC":{"format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"screen_name":"Costa Rican Col\u00f3n"},"CVE":{"format":"{symbol}{amount}","symbol":"$","offset":1,"screen_name":"Cape Verde Escudo"},"CZK":{"format":"{amount} {symbol}","symbol":"K\u010d","offset":100,"screen_name":"Czech Koruna"},"DJF":{"format":"{symbol}{amount}","symbol":"Fdj","offset":1,"screen_name":"Djiboutian Franc"},"DKK":{"format":"{amount} {symbol}","symbol":"kr.","offset":100,"screen_name":"Danish Krone"},"DOP":{"format":"{symbol} {amount}","symbol":"RD$","offset":100,"screen_name":"Dominican Peso"},"DZD":{"format":"{amount} {symbol}","symbol":"DA","offset":100,"screen_name":"Algerian Dinar"},"EGP":{"format":"{symbol} {amount}","symbol":"\u062c.\u0645.","offset":100,"screen_name":"Egyptian Pound"},"ERN":{"format":"{symbol}{amount}","symbol":"Nfk","offset":100,"screen_name":"Eritrean Nakfa"},"ETB":{"format":"{symbol}{amount}","symbol":"Br","offset":100,"screen_name":"Ethiopian Birr"},"EUR":{"format":"{symbol} {amount}","symbol":"\u20ac","offset":100,"screen_name":"Euro"},"FBZ":{"format":"{symbol}{amount}","symbol":"C","offset":100,"screen_name":"credits"},"FJD":{"format":"{symbol}{amount}","symbol":"FJ$","offset":100,"screen_name":"Fiji Dollar"},"FKP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"Falkland Islands Pound"},"GBP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"British Pound"},"GEL":{"format":"{amount} {symbol}","symbol":"\u20be","offset":100,"screen_name":"Georgian Lari"},"GHS":{"format":"{symbol}{amount}","symbol":"GHS","offset":100,"screen_name":"Ghanaian Cedi"},"GIP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"Gibraltar Pound"},"GMD":{"format":"{symbol}{amount}","symbol":"D","offset":100,"screen_name":"Gambian Dalasi"},"GNF":{"format":"{symbol}{amount}","symbol":"FG","offset":1,"screen_name":"Guinean Franc"},"GTQ":{"format":"{symbol}{amount}","symbol":"Q","offset":100,"screen_name":"Guatemalan Quetzal"},"GYD":{"format":"{symbol}{amount}","symbol":"G$","offset":100,"screen_name":"Guyanese Dollar"},"HKD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Hong Kong Dollar"},"HNL":{"format":"{symbol} {amount}","symbol":"L.","offset":100,"screen_name":"Honduran Lempira"},"HRK":{"format":"{amount} {symbol}","symbol":"kn","offset":100,"screen_name":"Croatian Kuna"},"HTG":{"format":"{symbol}{amount}","symbol":"G","offset":100,"screen_name":"Haitian Gourde"},"HUF":{"format":"{amount} {symbol}","symbol":"Ft","offset":1,"screen_name":"Hungarian Forint"},"IDR":{"format":"{symbol} {amount}","symbol":"Rp","offset":1,"screen_name":"Indonesian Rupiah"},"ILS":{"format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"screen_name":"Israeli New Shekel"},"INR":{"format":"{symbol} {amount}","symbol":"\u20b9","offset":100,"screen_name":"Indian Rupee"},"IQD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0639.","offset":100,"screen_name":"Iraqi Dinar"},"ISK":{"format":"{amount} {symbol}","symbol":"kr.","offset":1,"screen_name":"Iceland Krona"},"JMD":{"format":"{symbol}{amount}","symbol":"J$","offset":100,"screen_name":"Jamaican Dollar"},"JOD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0627.","offset":100,"screen_name":"Jordanian Dinar"},"JPY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"screen_name":"Japanese Yen"},"KES":{"format":"{symbol}{amount}","symbol":"KSh","offset":100,"screen_name":"Kenyan Shilling"},"KGS":{"format":"{amount} {symbol}","symbol":"\u0441\u043e\u043c","offset":100,"screen_name":"Kyrgyzstani Som"},"KHR":{"format":"{symbol}{amount}","symbol":"\u17db","offset":100,"screen_name":"Cambodian Riel"},"KMF":{"format":"{symbol}{amount}","symbol":"CF","offset":1,"screen_name":"Comoro Franc"},"KRW":{"format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"screen_name":"Korean Won"},"KWD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0643.","offset":100,"screen_name":"Kuwaiti Dinar"},"KYD":{"format":"{symbol}{amount}","symbol":"CI$","offset":100,"screen_name":"Cayman Islands Dollar"},"KZT":{"format":"{symbol}{amount}","symbol":"\u0422","offset":100,"screen_name":"Kazakhstani Tenge"},"LAK":{"format":"{symbol}{amount}","symbol":"\u20ad","offset":100,"screen_name":"Lao Kip"},"LBP":{"format":"{amount} {symbol}","symbol":"\u0644.\u0644.","offset":100,"screen_name":"Lebanese Pound"},"LKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Sri Lankan Rupee"},"LRD":{"format":"{symbol}{amount}","symbol":"L$","offset":100,"screen_name":"Liberian Dollar"},"LSL":{"format":"{symbol}{amount}","symbol":"M","offset":100,"screen_name":"Lesotho Loti"},"LTL":{"format":"{amount} {symbol}","symbol":"Lt","offset":100,"screen_name":"Lithuanian Litas"},"LVL":{"format":"{symbol} {amount}","symbol":"Ls","offset":100,"screen_name":"Latvian Lats"},"LYD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0644.","offset":100,"screen_name":"Libyan Dinar"},"MAD":{"format":"{amount} {symbol}","symbol":"\u062f.\u0645.","offset":100,"screen_name":"Moroccan Dirham"},"MDL":{"format":"{symbol}{amount}","symbol":"lei","offset":100,"screen_name":"Moldovan Leu"},"MGA":{"format":"{symbol}{amount}","symbol":"Ar","offset":5,"screen_name":"Malagasy Ariary"},"MKD":{"format":"{amount} {symbol}","symbol":"\u0434\u0435\u043d.","offset":100,"screen_name":"Macedonian Denar"},"MMK":{"format":"{symbol}{amount}","symbol":"Ks","offset":100,"screen_name":"Burmese Kyat"},"MNT":{"format":"{amount}{symbol}","symbol":"\u20ae","offset":100,"screen_name":"Mongolian Tugrik"},"MOP":{"format":"{symbol}{amount}","symbol":"MOP","offset":100,"screen_name":"Macau Patacas"},"MRO":{"format":"{symbol}{amount}","symbol":"UM","offset":5,"screen_name":"Mauritanian Ouguiya"},"MUR":{"format":"{symbol}{amount}","symbol":"\u20a8","offset":100,"screen_name":"Mauritian Rupee"},"MVR":{"format":"{symbol}{amount}","symbol":"\u0783.","offset":100,"screen_name":"Maldivian Rufiyaa"},"MWK":{"format":"{symbol}{amount}","symbol":"MK","offset":100,"screen_name":"Malawian Kwacha"},"MXN":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Mexican Peso"},"MYR":{"format":"{symbol}{amount}","symbol":"RM","offset":100,"screen_name":"Malaysian Ringgit"},"MZN":{"format":"{symbol}{amount}","symbol":"MT","offset":100,"screen_name":"Mozambican Metical"},"NAD":{"format":"{symbol}{amount}","symbol":"N$","offset":100,"screen_name":"Namibian Dollar"},"NGN":{"format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"screen_name":"Nigerian Naira"},"NIO":{"format":"{symbol} {amount}","symbol":"C$","offset":100,"screen_name":"Nicaraguan Cordoba"},"NOK":{"format":"{symbol} {amount}","symbol":"kr","offset":100,"screen_name":"Norwegian Krone"},"NPR":{"format":"{symbol}{amount}","symbol":"\u0930\u0942","offset":100,"screen_name":"Nepalese Rupee"},"NZD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"New Zealand Dollar"},"OMR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0639.","offset":100,"screen_name":"Omani Rial"},"PAB":{"format":"{symbol} {amount}","symbol":"B\/.","offset":100,"screen_name":"Panamanian Balboas"},"PEN":{"format":"{symbol} {amount}","symbol":"S\/","offset":100,"screen_name":"Peruvian Nuevo Sol"},"PGK":{"format":"{symbol}{amount}","symbol":"K","offset":100,"screen_name":"Papua New Guinean Kina"},"PHP":{"format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"screen_name":"Philippine Peso"},"PKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Pakistani Rupee"},"PLN":{"format":"{amount} {symbol}","symbol":"z\u0142","offset":100,"screen_name":"Polish Zloty"},"PYG":{"format":"{symbol} {amount}","symbol":"\u20b2","offset":1,"screen_name":"Paraguayan Guarani"},"QAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0642.","offset":100,"screen_name":"Qatari Rials"},"RON":{"format":"{amount} {symbol}","symbol":"lei","offset":100,"screen_name":"Romanian Leu"},"RSD":{"format":"{symbol}{amount}","symbol":"din","offset":100,"screen_name":"Serbian Dinar"},"RUB":{"format":"{amount} {symbol}","symbol":"p.","offset":100,"screen_name":"Russian Ruble"},"RWF":{"format":"{symbol}{amount}","symbol":"FRw","offset":1,"screen_name":"Rwandan Franc"},"SAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0633.","offset":100,"screen_name":"Saudi Arabian Riyal"},"SBD":{"format":"{symbol}{amount}","symbol":"SI$","offset":100,"screen_name":"Solomon Islands Dollar"},"SCR":{"format":"{symbol}{amount}","symbol":"SR","offset":100,"screen_name":"Seychelles Rupee"},"SEK":{"format":"{amount} {symbol}","symbol":"kr","offset":100,"screen_name":"Swedish Krona"},"SGD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Singapore Dollar"},"SHP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"Saint Helena Pound"},"SKK":{"format":"{amount} {symbol}","symbol":"Sk","offset":100,"screen_name":"Slovak Koruna"},"SLE":{"format":"{symbol}{amount}","symbol":"Le","offset":100,"screen_name":"Sierra Leonean Leone"},"SLL":{"format":"{symbol}{amount}","symbol":"Le","offset":100,"screen_name":"Sierra Leonean Old Leone"},"SOS":{"format":"{symbol}{amount}","symbol":"S","offset":100,"screen_name":"Somali Shilling"},"SRD":{"format":"{symbol}{amount}","symbol":"SRD","offset":100,"screen_name":"Surinamese Dollar"},"SSP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"South Sudanese Pound"},"STD":{"format":"{symbol}{amount}","symbol":"Db","offset":100,"screen_name":"Sao Tome and Principe Dobra"},"SVC":{"format":"{symbol}{amount}","symbol":"\u20a1","offset":100,"screen_name":"Salvadoran Col\u00f3n"},"SZL":{"format":"{symbol}{amount}","symbol":"L","offset":100,"screen_name":"Swazi Lilangeni"},"THB":{"format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"screen_name":"Thai Baht"},"TJS":{"format":"{symbol}{amount}","symbol":"\u0441.","offset":100,"screen_name":"Tajikistani Somoni"},"TMT":{"format":"{symbol}{amount}","symbol":"T","offset":100,"screen_name":"Turkmenistani Manat"},"TND":{"format":"{amount} {symbol}","symbol":"\u062f.\u062a.","offset":100,"screen_name":"Tunisian Dinar"},"TOP":{"format":"{symbol}{amount}","symbol":"T$","offset":100,"screen_name":"Tongan Pa\u02bbanga"},"TRY":{"format":"{amount} {symbol}","symbol":"TL","offset":100,"screen_name":"Turkish Lira"},"TTD":{"format":"{symbol}{amount}","symbol":"TT$","offset":100,"screen_name":"Trinidad and Tobago Dollar"},"TWD":{"format":"{symbol}{amount}","symbol":"NT$","offset":1,"screen_name":"Taiwan Dollar"},"TZS":{"format":"{symbol}{amount}","symbol":"TSh","offset":100,"screen_name":"Tanzanian Shilling"},"UAH":{"format":"{amount} {symbol}","symbol":"\u20b4","offset":100,"screen_name":"Ukrainian Hryvnia"},"UGX":{"format":"{symbol}{amount}","symbol":"USh","offset":1,"screen_name":"Ugandan Shilling"},"USD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"US Dollars"},"UYU":{"format":"{symbol} {amount}","symbol":"$U","offset":100,"screen_name":"Uruguayan Peso"},"UZS":{"format":"{amount} {symbol}","symbol":"\u0441\u045e\u043c","offset":100,"screen_name":"Uzbekistan Som"},"VEF":{"format":"{symbol} {amount}","symbol":"Bs.F","offset":100,"screen_name":"Venezuelan Bolivar"},"VES":{"format":"{symbol} {amount}","symbol":"Bs.S","offset":100,"screen_name":"Venezuelan Sovereign Bolivar"},"VND":{"format":"{amount} {symbol}","symbol":"\u20ab","offset":1,"screen_name":"Vietnamese Dong"},"VUV":{"format":"{symbol}{amount}","symbol":"VT","offset":1,"screen_name":"Vanuatu Vatu"},"WST":{"format":"{symbol}{amount}","symbol":"WS$","offset":100,"screen_name":"Samoan Tala"},"XAF":{"format":"{symbol}{amount}","symbol":"FCFA","offset":1,"screen_name":"Central African Frank"},"XCD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"East Caribbean Dollar"},"XOF":{"format":"{symbol}{amount}","symbol":"FCFA","offset":1,"screen_name":"West African Frank"},"XPF":{"format":"{symbol}{amount}","symbol":"\u20a3","offset":1,"screen_name":"CFP Franc"},"YER":{"format":"{amount} {symbol}","symbol":"\u0631.\u064a.","offset":100,"screen_name":"Yemeni Rial"},"ZAR":{"format":"{symbol} {amount}","symbol":"R","offset":100,"screen_name":"South African Rand"},"ZMW":{"format":"{symbol}{amount}","symbol":"K","offset":100,"screen_name":"Zambian Kwacha"},"ZWL":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Zimbabwean Dollar"}},"currency_map_for_cc":{"AED":{"format":"{amount} {symbol}","symbol":"\u062f.\u0625","offset":100,"screen_name":"UAE Dirham"},"ARS":{"format":"{symbol} {amount}","symbol":"$","offset":100,"screen_name":"Argentine Peso"},"AUD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Australian Dollar"},"BDT":{"format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"screen_name":"Bangladeshi Taka"},"BOB":{"format":"{symbol} {amount}","symbol":"Bs.","offset":100,"screen_name":"Bolivian Boliviano"},"BRL":{"format":"{symbol} {amount}","symbol":"R$","offset":100,"screen_name":"Brazilian Real"},"CAD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Canadian Dollar"},"CHF":{"format":"{symbol} {amount}","symbol":"Fr.","offset":100,"screen_name":"Swiss Franc"},"CLP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Chilean Peso"},"CNY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"screen_name":"Chinese Yuan"},"COP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Colombian Peso"},"CRC":{"format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"screen_name":"Costa Rican Col\u00f3n"},"CZK":{"format":"{amount} {symbol}","symbol":"K\u010d","offset":100,"screen_name":"Czech Koruna"},"DKK":{"format":"{amount} {symbol}","symbol":"kr.","offset":100,"screen_name":"Danish Krone"},"DZD":{"format":"{amount} {symbol}","symbol":"DA","offset":100,"screen_name":"Algerian Dinar"},"EGP":{"format":"{symbol} {amount}","symbol":"\u062c.\u0645.","offset":100,"screen_name":"Egyptian Pound"},"EUR":{"format":"{symbol} {amount}","symbol":"\u20ac","offset":100,"screen_name":"Euro"},"GBP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"British Pound"},"GTQ":{"format":"{symbol}{amount}","symbol":"Q","offset":100,"screen_name":"Guatemalan Quetzal"},"HKD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Hong Kong Dollar"},"HNL":{"format":"{symbol} {amount}","symbol":"L.","offset":100,"screen_name":"Honduran Lempira"},"HUF":{"format":"{amount} {symbol}","symbol":"Ft","offset":1,"screen_name":"Hungarian Forint"},"IDR":{"format":"{symbol} {amount}","symbol":"Rp","offset":1,"screen_name":"Indonesian Rupiah"},"ILS":{"format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"screen_name":"Israeli New Shekel"},"INR":{"format":"{symbol} {amount}","symbol":"\u20b9","offset":100,"screen_name":"Indian Rupee"},"ISK":{"format":"{amount} {symbol}","symbol":"kr.","offset":1,"screen_name":"Iceland Krona"},"JPY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"screen_name":"Japanese Yen"},"KES":{"format":"{symbol}{amount}","symbol":"KSh","offset":100,"screen_name":"Kenyan Shilling"},"KRW":{"format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"screen_name":"Korean Won"},"MOP":{"format":"{symbol}{amount}","symbol":"MOP","offset":100,"screen_name":"Macau Patacas"},"MXN":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Mexican Peso"},"MYR":{"format":"{symbol}{amount}","symbol":"RM","offset":100,"screen_name":"Malaysian Ringgit"},"NGN":{"format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"screen_name":"Nigerian Naira"},"NIO":{"format":"{symbol} {amount}","symbol":"C$","offset":100,"screen_name":"Nicaraguan Cordoba"},"NOK":{"format":"{symbol} {amount}","symbol":"kr","offset":100,"screen_name":"Norwegian Krone"},"NZD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"New Zealand Dollar"},"PEN":{"format":"{symbol} {amount}","symbol":"S\/","offset":100,"screen_name":"Peruvian Nuevo Sol"},"PHP":{"format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"screen_name":"Philippine Peso"},"PKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Pakistani Rupee"},"PLN":{"format":"{amount} {symbol}","symbol":"z\u0142","offset":100,"screen_name":"Polish Zloty"},"PYG":{"format":"{symbol} {amount}","symbol":"\u20b2","offset":1,"screen_name":"Paraguayan Guarani"},"QAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0642.","offset":100,"screen_name":"Qatari Rials"},"RON":{"format":"{amount} {symbol}","symbol":"lei","offset":100,"screen_name":"Romanian Leu"},"RUB":{"format":"{amount} {symbol}","symbol":"p.","offset":100,"screen_name":"Russian Ruble"},"SAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0633.","offset":100,"screen_name":"Saudi Arabian Riyal"},"SEK":{"format":"{amount} {symbol}","symbol":"kr","offset":100,"screen_name":"Swedish Krona"},"SGD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Singapore Dollar"},"THB":{"format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"screen_name":"Thai Baht"},"TRY":{"format":"{amount} {symbol}","symbol":"TL","offset":100,"screen_name":"Turkish Lira"},"TWD":{"format":"{symbol}{amount}","symbol":"NT$","offset":1,"screen_name":"Taiwan Dollar"},"USD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"US Dollars"},"UYU":{"format":"{symbol} {amount}","symbol":"$U","offset":100,"screen_name":"Uruguayan Peso"},"VND":{"format":"{amount} {symbol}","symbol":"\u20ab","offset":1,"screen_name":"Vietnamese Dong"},"ZAR":{"format":"{symbol} {amount}","symbol":"R","offset":100,"screen_name":"South African Rand"}},"currency_map_for_ads":{"AED":{"format":"{amount} {symbol}","symbol":"\u062f.\u0625","offset":100,"screen_name":"UAE Dirham"},"ARS":{"format":"{symbol} {amount}","symbol":"$","offset":100,"screen_name":"Argentine Peso"},"AUD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Australian Dollar"},"BDT":{"format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"screen_name":"Bangladeshi Taka"},"BOB":{"format":"{symbol} {amount}","symbol":"Bs.","offset":100,"screen_name":"Bolivian Boliviano"},"BRL":{"format":"{symbol} {amount}","symbol":"R$","offset":100,"screen_name":"Brazilian Real"},"CAD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Canadian Dollar"},"CHF":{"format":"{symbol} {amount}","symbol":"Fr.","offset":100,"screen_name":"Swiss Franc"},"CLP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Chilean Peso"},"CNY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"screen_name":"Chinese Yuan"},"COP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Colombian Peso"},"CRC":{"format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"screen_name":"Costa Rican Col\u00f3n"},"CZK":{"format":"{amount} {symbol}","symbol":"K\u010d","offset":100,"screen_name":"Czech Koruna"},"DKK":{"format":"{amount} {symbol}","symbol":"kr.","offset":100,"screen_name":"Danish Krone"},"DZD":{"format":"{amount} {symbol}","symbol":"DA","offset":100,"screen_name":"Algerian Dinar"},"EGP":{"format":"{symbol} {amount}","symbol":"\u062c.\u0645.","offset":100,"screen_name":"Egyptian Pound"},"EUR":{"format":"{symbol} {amount}","symbol":"\u20ac","offset":100,"screen_name":"Euro"},"GBP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"British Pound"},"GTQ":{"format":"{symbol}{amount}","symbol":"Q","offset":100,"screen_name":"Guatemalan Quetzal"},"HKD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Hong Kong Dollar"},"HNL":{"format":"{symbol} {amount}","symbol":"L.","offset":100,"screen_name":"Honduran Lempira"},"HUF":{"format":"{amount} {symbol}","symbol":"Ft","offset":1,"screen_name":"Hungarian Forint"},"IDR":{"format":"{symbol} {amount}","symbol":"Rp","offset":1,"screen_name":"Indonesian Rupiah"},"ILS":{"format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"screen_name":"Israeli New Shekel"},"INR":{"format":"{symbol} {amount}","symbol":"\u20b9","offset":100,"screen_name":"Indian Rupee"},"ISK":{"format":"{amount} {symbol}","symbol":"kr.","offset":1,"screen_name":"Iceland Krona"},"JPY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"screen_name":"Japanese Yen"},"KES":{"format":"{symbol}{amount}","symbol":"KSh","offset":100,"screen_name":"Kenyan Shilling"},"KRW":{"format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"screen_name":"Korean Won"},"MOP":{"format":"{symbol}{amount}","symbol":"MOP","offset":100,"screen_name":"Macau Patacas"},"MXN":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Mexican Peso"},"MYR":{"format":"{symbol}{amount}","symbol":"RM","offset":100,"screen_name":"Malaysian Ringgit"},"NGN":{"format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"screen_name":"Nigerian Naira"},"NIO":{"format":"{symbol} {amount}","symbol":"C$","offset":100,"screen_name":"Nicaraguan Cordoba"},"NOK":{"format":"{symbol} {amount}","symbol":"kr","offset":100,"screen_name":"Norwegian Krone"},"NZD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"New Zealand Dollar"},"PEN":{"format":"{symbol} {amount}","symbol":"S\/","offset":100,"screen_name":"Peruvian Nuevo Sol"},"PHP":{"format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"screen_name":"Philippine Peso"},"PKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Pakistani Rupee"},"PLN":{"format":"{amount} {symbol}","symbol":"z\u0142","offset":100,"screen_name":"Polish Zloty"},"PYG":{"format":"{symbol} {amount}","symbol":"\u20b2","offset":1,"screen_name":"Paraguayan Guarani"},"QAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0642.","offset":100,"screen_name":"Qatari Rials"},"RON":{"format":"{amount} {symbol}","symbol":"lei","offset":100,"screen_name":"Romanian Leu"},"RUB":{"format":"{amount} {symbol}","symbol":"p.","offset":100,"screen_name":"Russian Ruble"},"SAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0633.","offset":100,"screen_name":"Saudi Arabian Riyal"},"SEK":{"format":"{amount} {symbol}","symbol":"kr","offset":100,"screen_name":"Swedish Krona"},"SGD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Singapore Dollar"},"THB":{"format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"screen_name":"Thai Baht"},"TRY":{"format":"{amount} {symbol}","symbol":"TL","offset":100,"screen_name":"Turkish Lira"},"TWD":{"format":"{symbol}{amount}","symbol":"NT$","offset":1,"screen_name":"Taiwan Dollar"},"UAH":{"format":"{amount} {symbol}","symbol":"\u20b4","offset":100,"screen_name":"Ukrainian Hryvnia"},"USD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"US Dollars"},"UYU":{"format":"{symbol} {amount}","symbol":"$U","offset":100,"screen_name":"Uruguayan Peso"},"VND":{"format":"{amount} {symbol}","symbol":"\u20ab","offset":1,"screen_name":"Vietnamese Dong"},"ZAR":{"format":"{symbol} {amount}","symbol":"R","offset":100,"screen_name":"South African Rand"}},"currency_map_for_ads_backend":{"AED":{"format":"{amount} {symbol}","symbol":"\u062f.\u0625","offset":100,"screen_name":"UAE Dirham"},"ARS":{"format":"{symbol} {amount}","symbol":"$","offset":100,"screen_name":"Argentine Peso"},"AUD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Australian Dollar"},"BDT":{"format":"{symbol}{amount}","symbol":"\u09f3","offset":100,"screen_name":"Bangladeshi Taka"},"BOB":{"format":"{symbol} {amount}","symbol":"Bs.","offset":100,"screen_name":"Bolivian Boliviano"},"BRL":{"format":"{symbol} {amount}","symbol":"R$","offset":100,"screen_name":"Brazilian Real"},"CAD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Canadian Dollar"},"CHF":{"format":"{symbol} {amount}","symbol":"Fr.","offset":100,"screen_name":"Swiss Franc"},"CLP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Chilean Peso"},"CNY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":100,"screen_name":"Chinese Yuan"},"COP":{"format":"{symbol} {amount}","symbol":"$","offset":1,"screen_name":"Colombian Peso"},"CRC":{"format":"{symbol}{amount}","symbol":"\u20a1","offset":1,"screen_name":"Costa Rican Col\u00f3n"},"CZK":{"format":"{amount} {symbol}","symbol":"K\u010d","offset":100,"screen_name":"Czech Koruna"},"DKK":{"format":"{amount} {symbol}","symbol":"kr.","offset":100,"screen_name":"Danish Krone"},"DZD":{"format":"{amount} {symbol}","symbol":"DA","offset":100,"screen_name":"Algerian Dinar"},"EGP":{"format":"{symbol} {amount}","symbol":"\u062c.\u0645.","offset":100,"screen_name":"Egyptian Pound"},"EUR":{"format":"{symbol} {amount}","symbol":"\u20ac","offset":100,"screen_name":"Euro"},"GBP":{"format":"{symbol}{amount}","symbol":"\u00a3","offset":100,"screen_name":"British Pound"},"GTQ":{"format":"{symbol}{amount}","symbol":"Q","offset":100,"screen_name":"Guatemalan Quetzal"},"HKD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Hong Kong Dollar"},"HNL":{"format":"{symbol} {amount}","symbol":"L.","offset":100,"screen_name":"Honduran Lempira"},"HUF":{"format":"{amount} {symbol}","symbol":"Ft","offset":1,"screen_name":"Hungarian Forint"},"IDR":{"format":"{symbol} {amount}","symbol":"Rp","offset":1,"screen_name":"Indonesian Rupiah"},"ILS":{"format":"{symbol}{amount}","symbol":"\u20aa","offset":100,"screen_name":"Israeli New Shekel"},"INR":{"format":"{symbol} {amount}","symbol":"\u20b9","offset":100,"screen_name":"Indian Rupee"},"ISK":{"format":"{amount} {symbol}","symbol":"kr.","offset":1,"screen_name":"Iceland Krona"},"JPY":{"format":"{symbol}{amount}","symbol":"\u00a5","offset":1,"screen_name":"Japanese Yen"},"KES":{"format":"{symbol}{amount}","symbol":"KSh","offset":100,"screen_name":"Kenyan Shilling"},"KRW":{"format":"{symbol}{amount}","symbol":"\u20a9","offset":1,"screen_name":"Korean Won"},"LKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Sri Lankan Rupee"},"MOP":{"format":"{symbol}{amount}","symbol":"MOP","offset":100,"screen_name":"Macau Patacas"},"MXN":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Mexican Peso"},"MYR":{"format":"{symbol}{amount}","symbol":"RM","offset":100,"screen_name":"Malaysian Ringgit"},"NGN":{"format":"{symbol}{amount}","symbol":"\u20a6","offset":100,"screen_name":"Nigerian Naira"},"NIO":{"format":"{symbol} {amount}","symbol":"C$","offset":100,"screen_name":"Nicaraguan Cordoba"},"NOK":{"format":"{symbol} {amount}","symbol":"kr","offset":100,"screen_name":"Norwegian Krone"},"NZD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"New Zealand Dollar"},"PEN":{"format":"{symbol} {amount}","symbol":"S\/","offset":100,"screen_name":"Peruvian Nuevo Sol"},"PHP":{"format":"{symbol}{amount}","symbol":"\u20b1","offset":100,"screen_name":"Philippine Peso"},"PKR":{"format":"{symbol}{amount}","symbol":"Rs","offset":100,"screen_name":"Pakistani Rupee"},"PLN":{"format":"{amount} {symbol}","symbol":"z\u0142","offset":100,"screen_name":"Polish Zloty"},"PYG":{"format":"{symbol} {amount}","symbol":"\u20b2","offset":1,"screen_name":"Paraguayan Guarani"},"QAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0642.","offset":100,"screen_name":"Qatari Rials"},"RON":{"format":"{amount} {symbol}","symbol":"lei","offset":100,"screen_name":"Romanian Leu"},"SAR":{"format":"{amount} {symbol}","symbol":"\u0631.\u0633.","offset":100,"screen_name":"Saudi Arabian Riyal"},"SEK":{"format":"{amount} {symbol}","symbol":"kr","offset":100,"screen_name":"Swedish Krona"},"SGD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"Singapore Dollar"},"THB":{"format":"{symbol}{amount}","symbol":"\u0e3f","offset":100,"screen_name":"Thai Baht"},"TRY":{"format":"{amount} {symbol}","symbol":"TL","offset":100,"screen_name":"Turkish Lira"},"TWD":{"format":"{symbol}{amount}","symbol":"NT$","offset":1,"screen_name":"Taiwan Dollar"},"UAH":{"format":"{amount} {symbol}","symbol":"\u20b4","offset":100,"screen_name":"Ukrainian Hryvnia"},"USD":{"format":"{symbol}{amount}","symbol":"$","offset":100,"screen_name":"US Dollars"},"UYU":{"format":"{symbol} {amount}","symbol":"$U","offset":100,"screen_name":"Uruguayan Peso"},"VND":{"format":"{amount} {symbol}","symbol":"\u20ab","offset":1,"screen_name":"Vietnamese Dong"},"ZAR":{"format":"{symbol} {amount}","symbol":"R","offset":100,"screen_name":"South African Rand"}},"currency_locale_format":{"AE":"{amount} {symbol}","AL":"{amount}{symbol}","AM":"{amount} {symbol}","AR":"{symbol} {amount}","AT":"{symbol} {amount}","AU":"{symbol}{amount}","AZ":"{amount} {symbol}","BE":"{amount} {symbol}","BG":"{amount} {symbol}","BH":"{amount} {symbol}","BN":"","BO":"{symbol} {amount}","BR":"{symbol} {amount}","BY":"{amount} {symbol}","BZ":"","CA":"{symbol}{amount}","CH":"{symbol} {amount}","CL":"{symbol} {amount}","CN":"{symbol}{amount}","CO":"{symbol} {amount}","CR":"{symbol}{amount}","CZ":"{amount} {symbol}","DE":"{amount} {symbol}","DK":"{amount} {symbol}","DO":"{symbol} {amount}","DZ":"{amount} {symbol}","EC":"{symbol} {amount}","EE":"{amount} {symbol}","EG":"{symbol} {amount}","ES":"{amount} {symbol}","FI":"{amount} {symbol}","FO":"{symbol} {amount}","FR":"{amount} {symbol}","GB":"{symbol}{amount}","GE":"{amount} {symbol}","GR":"{amount} {symbol}","GT":"{symbol}{amount}","HK":"{symbol}{amount}","HN":"{symbol} {amount}","HR":"{amount} {symbol}","HU":"{amount} {symbol}","ID":"{symbol} {amount}","IE":"{symbol} {amount}","IL":"{symbol}{amount}","IN":"{symbol} {amount}","IQ":"{amount} {symbol}","IR":"{amount} {symbol}","IS":"{amount} {symbol}","IT":"{symbol} {amount}","JM":"{symbol}{amount}","JO":"{amount} {symbol}","JP":"{symbol}{amount}","KE":"{symbol}{amount}","KG":"{amount} {symbol}","KR":"{symbol}{amount}","KW":"{amount} {symbol}","KZ":"{symbol}{amount}","LB":"{amount} {symbol}","LI":"{symbol} {amount}","LT":"{amount} {symbol}","LU":"{amount} {symbol}","LV":"{symbol} {amount}","LY":"{amount} {symbol}","MA":"{amount} {symbol}","MC":"{amount} {symbol}","MK":"{amount} {symbol}","MN":"{amount}{symbol}","MO":"{symbol}{amount}","MV":"","MX":"{symbol}{amount}","MY":"{symbol}{amount}","NI":"{symbol} {amount}","NL":"{symbol} {amount}","NO":"{symbol} {amount}","NZ":"{symbol}{amount}","OM":"{amount} {symbol}","PA":"{symbol} {amount}","PE":"{symbol} {amount}","PH":"{symbol}{amount}","PK":"{symbol}{amount}","PL":"{amount} {symbol}","PR":"{symbol} {amount}","PT":"{amount} {symbol}","PY":"{symbol} {amount}","QA":"{amount} {symbol}","RO":"{amount} {symbol}","RU":"{amount} {symbol}","SA":"{amount} {symbol}","SE":"{amount} {symbol}","SG":"{symbol}{amount}","SI":"{amount} {symbol}","SK":"{amount} {symbol}","SV":"{symbol}{amount}","SY":"{amount} {symbol}","TH":"{symbol}{amount}","TN":"{amount} {symbol}","TR":"{amount} {symbol}","TT":"","TW":"{symbol}{amount}","UA":"{amount} {symbol}","US":"{symbol}{amount}","UY":"{symbol} {amount}","UZ":"{amount} {symbol}","VE":"{symbol} {amount}","VN":"{amount} {symbol}","YE":"{amount} {symbol}","ZA":"{symbol} {amount}","ZW":""}},745],["TimeSliceInteractionSV",[],{"on_demand_reference_counting":true,"on_demand_profiling_counters":true,"default_rate":1000,"lite_default_rate":100,"interaction_to_lite_coinflip":{"ADS_INTERFACES_INTERACTION":0,"ads_perf_scenario":0,"ads_wait_time":0,"Event":1},"interaction_to_coinflip":{"ADS_INTERFACES_INTERACTION":1,"ads_perf_scenario":1,"ads_wait_time":1,"Event":100},"enable_heartbeat":false,"maxBlockMergeDuration":0,"maxBlockMergeDistance":0,"enable_banzai_stream":true,"user_timing_coinflip":50,"banzai_stream_coinflip":0,"compression_enabled":true,"ref_counting_fix":false,"ref_counting_cont_fix":false,"also_record_new_timeslice_format":false,"force_async_request_tracing_on":false},2609],["IntlCompactDecimalNumberFormatConfig",[],{"short_patterns":{"3":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"}},"4":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"}},"5":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"K","negative_prefix_pattern":"-","negative_suffix_pattern":"K"}},"6":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"}},"7":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"}},"8":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"M","negative_prefix_pattern":"-","negative_suffix_pattern":"M"}},"9":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"}},"10":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"}},"11":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"B","negative_prefix_pattern":"-","negative_suffix_pattern":"B"}},"12":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"}},"13":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"}},"14":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":"T","negative_prefix_pattern":"-","negative_suffix_pattern":"T"}}},"long_patterns":{"3":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"}},"4":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"}},"5":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" thousand","negative_prefix_pattern":"-","negative_suffix_pattern":" thousand"}},"6":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"}},"7":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"}},"8":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" million","negative_prefix_pattern":"-","negative_suffix_pattern":" million"}},"9":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"}},"10":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"}},"11":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" billion","negative_prefix_pattern":"-","negative_suffix_pattern":" billion"}},"12":{"4":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"},"24":{"min_fraction_digits":null,"min_integer_digits":1,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"}},"13":{"4":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"},"24":{"min_fraction_digits":null,"min_integer_digits":2,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"}},"14":{"4":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"},"24":{"min_fraction_digits":null,"min_integer_digits":3,"positive_prefix_pattern":"","positive_suffix_pattern":" trillion","negative_prefix_pattern":"-","negative_suffix_pattern":" trillion"}}}},2981],["VideoPlayerStateBasedLoggingEvents",[],{"StateBasedLoggingEventNames":["cancelled_requested_playing","caption_change","chromecast_availability_checked","chromecast_not_supported","chromecast_button_visible","entered_fs","entered_hd","error","exited_fs","exited_hd","finished_loading","finished_playing","headset_connected","headset_disconnected","host_error","muted","paused","player_format_changed","quality_change","requested_playing","scrubbed","seeked","started_playing","unmuted","unpaused","volume_changed","volume_decrease","volume_increase","viewport_rotated","viewport_zoomed","heading_reset","guide_entered","guide_exited","spherical_fallback_entered","played_for_three_seconds","commercial_break_offscreen","commercial_break_onscreen","ad_break_starting_indicator","ad_break_non_interruptive_ad_start","ad_break_non_interruptive_ad_click","ad_break_pre_roll_ad_start","ad_break_tap_on_trailer","ad_break_tap_start_from_trailer","representation_ended","heart_beat","stale","viewability_changed","video_logging_session_timeout","video_logging_session_wakeup","retry_on_error","playback_speed_changed","video_warmup_evicted","suppress_video_off","suppress_video_on","error_recovery_attempt","displayed_frames","video_viewability_updated"]},3462],["VideoPlayerContextSensitiveConfigPayload",[],{"context_sensitive_values":{"buffering_overflow_threshold":[{"value":3.0832147598267,"contexts":[{"name":"connection_quality","value":"POOR"}]},{"value":3.0832147598267,"contexts":[{"name":"connection_quality","value":"MODERATE"}]}],"enable_request_pipelining_for_live":[{"value":false,"contexts":[{"name":"latency_level","value":"low"}]}],"initial_stream_buffer_size_float":[{"value":10,"contexts":[{"name":"is_ad","value":true}]}],"is_low_latency":[{"value":true,"contexts":[{"name":"latency_level","value":"low"}]}],"live_stream_buffer_size_float":[{"value":10,"contexts":[{"name":"content_category","value":"esports"}]},{"value":6.5,"contexts":[{"name":"content_category","value":"gaming"}]}],"num_predictive_segments":[{"value":4,"contexts":[{"name":"latency_level","value":"low"}]}],"oz_www_append_byte_target_without_range":[{"value":1,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_bandwidth_ignore_on_stream_write_samples":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_bandwidth_use_response_time_adjustment":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_buffer_ahead_target":[{"value":10,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"content_category","value":"gaming"}]},{"value":14,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]},{"value":24,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_catchup_timeout_after_buffering_sec":[{"value":0.001,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_catchup_timeout_after_play_sec":[{"value":30,"contexts":[{"name":"fbls_tier","value":"user"}]}],"oz_www_ignore_reset_after_seek_if_bufferahead":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_in_play_buffer_overflow_target":[{"value":1,"contexts":[{"name":"connection_quality","value":"POOR"}]},{"value":1,"contexts":[{"name":"connection_quality","value":"MODERATE"}]}],"oz_www_latencymanager_stalled_edgelatency_sec":[{"value":2,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]},{"value":6,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"content_category","value":"gaming"}]},{"value":10,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]},{"value":18,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_latencymanager_stalled_edgelatency_sec_on":[{"value":false,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]},{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_live_initial_playback_position":[{"value":-14,"contexts":[{"name":"content_category","value":"gaming"}]}],"oz_www_live_rewind_seek_to_live_delta":[{"value":4,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]}],"oz_www_minimum_bytes_to_sample_on_close":[{"value":10000,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_ms_promise_for_null":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_ms_promise_for_null_ms":[{"value":1000,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_network_reload_mpd_json":[{"value":"[\"504\", \"404\"]","contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_network_retry_intervals_json":[{"value":"{\"0\": 1000, \"404\": 2000, \"502\": 1000, \"503\": 1000, \"504\": 1000, \"20\": 1, \"429\": 2000}","contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_overwrite_live_time_range_block_margin":[{"value":5,"contexts":[{"name":"latency_level","value":"low"},{"name":"fbls_tier","value":"user"}]}],"oz_www_overwrite_livehead_fall_behind_block_threshold":[{"value":10,"contexts":[{"name":"latency_level","value":"low"}]}],"oz_www_pdash_download_cursor_catchup_threshold_gop_multiplier":[{"value":1,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_pdash_download_cursor_catchup_threshold_sec":[{"value":1,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]},{"value":2,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true},{"name":"latency_level","value":"low"}]},{"value":6,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]},{"value":10,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"low"}]},{"value":10,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_pdash_download_cursor_catchup_tolerance_sec":[{"value":1,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_pdash_use_pdash_segmentlocator":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_per_stream_duration_target":[{"value":0.5,"contexts":[{"name":"connection_quality","value":"GOOD"}]},{"value":0.5,"contexts":[{"name":"connection_quality","value":"EXCELLENT"}]}],"oz_www_playback_speed_latency_slowdown_adjustment_rate":[{"value":0.2,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_playback_speed_latency_speedup_adjustment_rate":[{"value":0.1,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_playback_speed_min_buffer_sec":[{"value":0.5,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]}],"oz_www_player_emit_mpdparsed_early":[{"value":true,"contexts":[{"name":"is_live","value":true}]}],"oz_www_player_emit_mpdready_early":[{"value":true,"contexts":[{"name":"is_live","value":true}]}],"oz_www_player_formats_for_low_latency":[{"value":"[\"*\"]","contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_playhead_manager_buffered_auto_seek_playhead_slack":[{"value":0.01,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_playhead_manager_buffered_numerical_error":[{"value":0,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_playhead_manager_clamp_initial_playback_position":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_pre_start_buffer_ahead_target":[{"value":10,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_reset_catchup_timeout_after_play_sec_on_overwrite":[{"value":true,"contexts":[{"name":"content_category","value":"gaming"}]},{"value":true,"contexts":[{"name":"fbls_tier","value":"user"}]}],"oz_www_seek_on_latency_level_change_allowed":[{"value":"[[\"low\",\"normal\"],[\"normal\",\"normal\"]]","contexts":[{"name":"content_category","value":"gaming"},{"name":"is_live","value":true}]}],"oz_www_skip_videobuffer_gaps":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_skip_videobuffer_gaps_on_buffer_updated":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_stale_mpd_buffer_ahead_target":[{"value":2,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_start_buffer_underflow_target":[{"value":1.1,"contexts":[{"name":"connection_quality","value":"POOR"}]},{"value":1.1,"contexts":[{"name":"connection_quality","value":"MODERATE"}]}],"oz_www_steadystate_minbuffer_buckets":[{"value":4,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]},{"value":20,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]}],"oz_www_steadystate_minbuffer_sec":[{"value":1,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"latency_level","value":"ultra-low"}]},{"value":1.75,"contexts":[{"name":"streaming_implementation","value":"pdash"},{"name":"is_latency_sensitive_broadcast","value":true}]}],"oz_www_stream_types_eligible_for_partial_playback":[{"value":"audio","contexts":[{"name":"is_live","value":true}]}],"oz_www_systemic_risk_abr_initial_risk_factor":[{"value":4,"contexts":[{"name":"is_live","value":true}]}],"oz_www_systemic_risk_abr_low_mos_resolution":[{"value":260,"contexts":[{"name":"is_live","value":true}]}],"oz_www_time_to_first_byte_ignore_above_threshold_ms":[{"value":200,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}],"oz_www_use_live_latency_manager":[{"value":true,"contexts":[{"name":"streaming_implementation","value":"pdash"}]}]},"static_values":{"stream_buffer_size_float":15,"initial_stream_buffer_size_float":7.5,"min_switch_interval":5000,"min_eval_interval":1000,"bandwidth_upgrade_target":0.9,"live_bandwidth_upgrade_target":0.63308577239513,"resolution_constraint_factor":2,"resolve_video_time_update_on_fragmented_time_ranges":true,"live_send_push_headers":false,"live_initial_stream_buffer_size_float":3.5,"enable_video_debug":false,"reappend_init_segment_after_abort":false,"current_time_during_ready_state_zero_throws":false,"www_videos_nudge_timestamp_correction_s":0,"www_videos_extended_spl_logging_for_connected_tv":false,"live_abr_send_push_headers":false,"shaka_native_promise":false,"oz_www_skip_videobuffer_gaps":true,"oz_www_skip_timerange_gaps":true,"oz_www_skip_videobuffer_gaps_on_buffer_updated":true,"oz_www_skip_videobuffer_gaps_max_gap_size_sec":0,"use_resource_timing_entry_for_bandwidth":false,"force_lowest_representation_threshold":0,"force_lower_representation_step_ratio":0,"enable_request_pipelining_for_vod":false,"enable_streaming_for_vod":false,"use_continuous_streaming":false,"streaming_segment_size":4,"streaming_bandwidth_update_interval":180000,"multi_segment_decay":0,"enable_request_pipelining_for_live":true,"enable_streaming_code_path":true,"ignore_errors_after_unload":true,"use_pending_seek_position_for_reference":false,"start_stream_buffer_size":1,"videos_abr_debugger_storage":false,"fix_shaka_xhr_error_status":true,"set_current_time_in_resync":true,"live_stream_buffer_size_float":3,"use_dimensions_fallbacks":true,"aggressive_fast_moving_average_half_life":1.5779530704021,"aggressive_slow_moving_average_half_life":9.2335087917745,"ignore_left_button_when_pausing":true,"disable_360_abr":false,"clear_buffer_on_seek_back":false,"buffering_overflow_threshold":0,"use_buffer_target_only_after_buffering_goal_is_hit":false,"enable_content_protection":true,"drop_buffering_detection_from_html5_api":true,"exclude_tracks_without_smooth_playback":false,"mpd_parse_frame_and_audio_sampling_rate":false,"abort_loading_decisioning_logic":true,"buffer_velocity_time_in_past_to_consider":0,"evaluate_abr_on_fetch_end":false,"ignore_recent_bandwidth_eval_on_fetch_end":false,"bandwidth_penalty_per_additional_video":0.1,"overwrite_video_current_time_property":false,"low_start_stream_buffer_size":2.5,"low_bandwidth_start_stream_buffer_size_threshold":2000000,"connection_quality_context_throttle_frequency":1000,"allow_seek_logging_in_mixin":false,"fix_pause_current_time_in_mixin":true,"better_set_current_time_in_resync":false,"resync_set_current_time_fudge_factor":0,"enable_main_thread_availability_logging":true,"fix_overwritten_get_video_current_time":false,"should_use_oz_p2p":false,"oz_www_enable_lip_sync_abr_select_quality":false,"oz_www_enable_alternative_audio_tracks":false,"oz_www_video_cdn_url_refresh":false,"oz_www_no_rep_to_switch_fallback_progressive":false,"oz_www_enable_revoke_object_url_on_destroy":false,"oz_www_enable_abr_logging":false,"oz_www_fix_oz_p2p_enable_failed":true,"num_predictive_segments":0,"oz_www_stream_reader_max_buffer_len":0,"oz_www_generate_mos_segment_buffer_diff":false,"oz_www_pdash_use_pdash_segmentlocator":false,"oz_www_pdash_seq_based_approx":false,"oz_www_pdash_seq_based_approx_use_blockedrange":false,"oz_www_pdash_download_cursor_catchup_threshold_sec":0,"oz_www_pdash_download_cursor_catchup_tolerance_sec":0,"oz_www_pdash_download_cursor_catchup_skip_totalbufer":true,"oz_www_pdash_download_cursor_catchup_threshold_gop_multiplier":0,"oz_www_pdash_download_cursor_tolerance_gop_multiplier":0,"oz_www_pdash_download_cursor_bufferahead_gop_multiplier":0,"oz_www_pdash_download_cursor_between_catchups_seg":0,"oz_www_latencymanager_stalempd_edgelatency_sec":0,"oz_www_latencymanager_stalempd_edgelatency_sec_on":false,"oz_www_latencymanager_stalled_edgelatency_sec_on":false,"oz_www_latencymanager_stalled_edgelatency_sec":0,"oz_www_pdash_future_edgelatency_gops":0,"oz_www_catchup_timeout_after_play_sec":0,"oz_www_catchup_timeout_after_buffering_sec":0,"oz_www_reset_catchup_timeout_after_play_sec_on_overwrite":true,"oz_www_catchup_min_interval_sec":0,"oz_www_playback_speed_latency_adjustment_disabled":false,"oz_www_playback_speed_latency_adjustment_rate":0,"oz_www_playback_speed_enabled_delay_sec":4,"oz_www_playback_speed_min_buffer_sec":1,"oz_www_playback_speed_min_duration_sec":2,"oz_www_playback_speed_restore_min_duration_sec":1,"oz_www_playback_speed_latency_slowdown_adjustment_rate":0,"oz_www_playback_speed_latency_speedup_adjustment_rate":0,"oz_www_pdash_download_cursor_use_totalbuffer":true,"oz_www_pdash_download_cursor_nocatchup_use_currentbuffer":false,"oz_www_playback_speed_min_sharpness_factor":3,"oz_www_pdash_wait_on_mpd_refresh_when_error":true,"oz_www_pdash_download_cursor_pause_duration_of_gop":false,"oz_www_pdash_download_cursor_catchup_only_when_advancing":false,"oz_www_download_cursor_use_node_time":false,"oz_www_download_cursor_total_buffer_max_sec":0,"oz_www_download_cursor_buffer_ahead_time_max_sec":0,"oz_www_download_cursor_disable_buffer_ahead_rule_on":true,"oz_www_download_cursor_disable_buffer_ahead_rule_lr_on":false,"oz_www_download_cursor_1st_run_set_skipped_segment_on":true,"oz_www_download_cursor_1st_run_2_fallback":false,"oz_www_enable_predictive_dash":true,"oz_www_use_sc_timebased_segments":false,"oz_www_use_templated_pdash_segments":true,"oz_www_use_scf_timebased_segments":false,"oz_www_touch_cb_key":false,"oz_www_enable_dvl":true,"oz_www_use_dvl_with_timeout_ms":0,"oz_www_dvl_update_interval_ms":0,"oz_www_dvl_update_interval_reset_on_err":true,"oz_www_dvl_initial_segment_ignore_count":1,"oz_www_use_inline_manifest_for_live":true,"oz_www_use_live_latency_manager":false,"oz_www_player_emit_mpdready_early":false,"oz_www_clear_on_seek":true,"oz_www_player_emit_mpdparsed_early":false,"oz_www_parse_number_templated_uri":true,"is_low_latency":false,"fire_seek_events":false,"shaka_buffer_abr":false,"evaluate_abr_on_tracks_and_bandwidth_change":true,"enable_double_ingest":true,"www_videos_playback_remove_src_attr_on_unload":false,"www_videos_playback_call_load_on_unload":false,"has_live_rewind_egress":false,"has_live_rewind_ui_www":false,"use_oz_with_fbms_eligible":true,"create_restore_abort_loading_promise":true,"oz_www_fix_representation_ended_timing":true,"oz_www_safely_log_player_seeks":true,"oz_www_enable_adaptation":true,"oz_www_fix_quick_starter_overhead":true,"oz_www_abr_restrict_from_index":0,"oz_www_abr_restrict_to_index":0,"oz_www_initial_switch_interval":0,"oz_www_min_switch_interval":100,"oz_www_abr_min_bandwidth_samples":0,"oz_www_min_eval_interval":100,"oz_www_bandwidth_estimator_half_life":6,"oz_www_bandwidth_estimator_variance_penalty_half_life":0,"oz_www_bandwidth_estimator_variance_penalty_down_factor":0,"oz_www_bandwidth_estimator_variance_penalty_up_factor":0,"oz_www_bandwidth_estimator_outlier_exclusion_factor":50,"oz_www_bandwidth_estimator_std_dev_penalty_factor":0,"oz_www_abr_confidence_threshold":0.9,"oz_www_segments_to_stream":5,"oz_www_per_stream_duration_target":0,"oz_www_segments_to_stream_under_playhead_threshold":0,"oz_www_low_segment_stream_playhead_threshold":0,"oz_www_appends_per_segment":6,"oz_www_append_byte_target_without_range":100000,"oz_www_log_appended_secs":false,"oz_www_lazy_parse_sidx":false,"oz_www_abr_eval_buffer_threshold":0,"oz_www_throttle_playback_rate_on_stall":true,"oz_www_use_prefetch_cache":true,"oz_www_force_initial_representation":true,"oz_www_buffer_ahead_target":22,"oz_www_low_buffer_bandwidth_target_threshold":10,"oz_www_low_buffer_bandwidth_target_increase_factor":0,"oz_www_segments_to_stream_near_bandwidth_boundary":5,"oz_www_bandwidth_boundary_standard_deviation_factor":1,"oz_www_suppress_playing_event_while_buffering":false,"oz_www_resolution_constraint_factor":2,"oz_www_pre_start_buffer_ahead_target":16.924449682236,"oz_www_stale_mpd_buffer_ahead_target":0,"oz_www_setup_buffer_target_onload":true,"oz_www_in_play_buffer_underflow_target":0.1,"oz_www_in_play_buffer_overflow_target":1,"oz_www_buffer_when_waiting":false,"oz_www_start_buffer_underflow_target":0.1,"oz_www_byte_count_per_sample":200000,"oz_www_minimum_bytes_to_sample_on_close":25000,"oz_www_manifest_update_frequency_ms":0,"oz_www_manifest_initial_update_delay_ms":0,"oz_www_exclude_prefetch_bandwidth_samples":true,"oz_www_connection_quality_context_throttle_frequency":2000,"oz_www_paused_stream_segments_count":2,"oz_www_minimum_bandwidth_sample_duration":10,"oz_www_maximum_bandwidth_sample_bandwidth":100000000,"oz_www_max_bandwidth_sample_count":30,"oz_www_use_performance_entry_on_stream_close":false,"oz_www_ignore_time_to_response_start":false,"oz_www_network_seg_timeout_ms":0,"oz_www_bandwidth_response_time_handicap":0,"oz_www_bandwidth_ignore_on_stream_write_samples":false,"oz_www_bandwidth_use_response_time_adjustment":false,"oz_www_use_scheduler":true,"oz_www_cdn_experiment_id":"","oz_www_disable_audio_scheduler":false,"oz_www_no_new_loop_body_promise_when_stream_ongoing":true,"oz_www_fix_seek_performance":false,"oz_www_ignore_reset_after_seek_if_bufferahead":false,"oz_www_ignore_reset_after_seek_if_bufferahead_liverewind":true,"oz_www_enable_network_manager_error":false,"oz_www_parse_first_segment":false,"oz_www_enable_abr_for_first_request":false,"oz_www_update_media_source_duration":true,"oz_www_ms_promise_for_null":false,"oz_www_ms_promise_for_null_ms":0,"oz_www_sbm_waits_for_update_end":true,"oz_www_sbm_cancel_operation_synchronous":false,"oz_www_sbm_recursively_waits_for_update_end":false,"oz_www_auto_seek_playhead_slack":0.5,"oz_www_playhead_manager_buffered_auto_seek_playhead_slack":0.5,"oz_www_playhead_manager_buffered_is_near_gap_threshold":1.5,"oz_www_playhead_manager_buffered_numerical_error":0.01,"oz_www_playhead_manager_handle_timerange_update_on_timeupdate":true,"oz_www_playhead_manager_buffered_timerange_update_on_timeupdate":true,"oz_www_playhead_manager_timeupdate_throttle_ms":1000,"oz_www_playhead_manager_clamp_initial_playback_position":false,"oz_www_playhead_manager_buffer_gaps_skip_reverse":true,"oz_www_media_stream_buffer_gaps_ignore_before_seek":false,"oz_www_playhead_manager_initial_playback_position_lat_mgr":true,"oz_www_playheadman_dont_skip_ahead_past_last_fetched":false,"oz_www_seek_ahead_epsilon":0.05,"oz_www_seek_ahead_use_native_current_time":true,"oz_www_timeline_offset_threshold":10,"oz_www_live_rewind_seek_to_live_delta":8,"oz_www_update_seekable_range":true,"oz_www_update_duration_when_init_appended":true,"oz_www_abr_prevent_down_switch_buffer_threshold":11,"oz_www_check_buffer_range_once_for_playhead_update":false,"oz_www_fix_start_timestamp":true,"oz_www_fix_templated_manifest_r_field_check":true,"oz_www_seek_on_latency_level_change":false,"oz_www_seek_on_latency_level_change_allowed":"[]","oz_www_live_initial_playback_position":-20,"oz_www_live_query_time_in_range":true,"oz_www_timerange_manager_cleanup":false,"oz_www_livehead_fall_behind_block_threshold":30,"oz_www_overwrite_livehead_fall_behind_block_threshold":0,"oz_www_live_time_range_block_margin":5.9,"oz_www_overwrite_live_time_range_block_margin":0,"oz_www_live_gracefully_handle_mpd_errors":true,"oz_www_live_no_segment_when_playhead_is_before_first_segment":true,"oz_www_live_disable_mpd_updates_when_paused":true,"oz_www_cleanup_video_node_on_destroy":true,"oz_www_detach_media_source_manager":true,"oz_www_enable_abortload_and_reload":true,"oz_www_live_playhead_catch_up":false,"oz_www_live_catch_up_only_when_paused":false,"oz_www_live_catch_up_fall_behind_threshold":20,"oz_www_live_catch_up_live_head_delta":6,"oz_www_live_numerical_error_epsilon":0.0001,"oz_www_stop_manifest_update_when_static":true,"oz_www_buffer_end_only_when_buffering":false,"oz_www_stream_interrupt_check_mpd_stale_count_threshold":6,"oz_www_reach_end_only_when_video_ended":false,"oz_www_allow_queueing_end_of_stream_when_update":false,"oz_www_set_source_buffer_append_window_end":false,"oz_www_use_stream_end_time_in_segment_locator":false,"oz_www_pausable_stream_throws_error_when_aborted":true,"oz_www_network_retry_intervals_json":"{\"0\": 1000, \"404\": 2000, \"502\": 1000, \"503\": 1000, \"504\": 1000, \"429\": 2000, \"20\": 1000}","oz_www_network_retry_intervals_json_retry":false,"oz_www_network_reload_mpd_json_retry":false,"oz_www_network_end_broadcasts_json":"[\"410\"]","oz_www_network_skip_json":"[]","oz_www_network_reload_mpd_json":"[]","oz_www_fallback_on_append_error":false,"oz_www_enable_appends_on_wait_update_end_failure":false,"oz_www_bandwidth_penalty_per_additional_video":0,"oz_www_bandwidth_penalty_additional_video_start":0,"oz_www_prefetch_cache_bandwidth_upper_limit":0,"oz_www_throw_network_error_during_stream":false,"oz_www_retry_fetch_on_prefetch_error":true,"oz_www_use_abr_for_missing_default_representation":true,"oz_www_initial_manifest_request_retry_count":3,"oz_www_download_time_buffer_delta_penalty_factor":0,"oz_www_time_to_first_byte_estimate_half_life_ms":500,"oz_www_handle_missing_manifest_segment_timeline":true,"oz_www_mos_upper_threshold":0,"oz_www_mos_lower_threshold":0,"oz_www_abr_use_download_time":false,"oz_www_minimum_download_additional_buffer_ms":0,"oz_www_use_deferred_streaming_task":true,"oz_www_allow_abort_loading_from_autoplay_controller":false,"oz_www_enable_double_ingest":true,"oz_www_use_oz_credentials_provider":true,"oz_www_throw_on_non_zero_r_d_mismatch":true,"oz_www_fix_template_duration_artifact_in_manifest":true,"oz_www_bandwidth_cache_key":"bandwidthEstimate","oz_www_estimate_video_bandwidth_only":true,"oz_www_default_bandwidth_estimate":1000000,"oz_www_update_bandwidth_cache_on_sample":false,"oz_www_live_audio_ibr_bandwidth_percentage":0.05,"comet_www_no_pause_on_blur_or_focus_events":false,"oz_www_live_gracefully_handle_no_network":true,"oz_www_live_max_try_attempts_on_404":2,"oz_www_clear_buffer_when_switch_representation_initiator_is_user":true,"oz_www_use_segment_request_cache":false,"oz_www_seconds_to_stream":10,"oz_www_seconds_to_stream_near_bandwidth_boundary":10,"oz_www_queue_data_with_error_handling":false,"oz_www_clear_prepended_segments_count_on_append":true,"oz_www_back_off_pdash_504_retry":true,"oz_www_call_end_of_stream_in_quick_starter":true,"oz_www_instantiate_buffering_detector_before_quick_starter":true,"oz_www_maybe_end_stream_if_prepended_segments":true,"oz_www_seek_to_start_quick_starter":true,"oz_www_use_loose_manifest_updates":false,"oz_www_use_full_player_if_cached":true,"oz_www_enable_quickdashv2":false,"oz_www_copy_new_manifest":true,"oz_www_handle_switch_to_unparsed_representation_sidx":true,"oz_www_stable_buffered_timeranges_in_observedsourcebufferstate":true,"oz_www_emit_stream_error_event":true,"oz_www_max_start_eme_attempts":3,"oz_www_listen_for_canplay_in_buffering_detector":true,"oz_www_handle_mpd_retries_outside_oz_mpd_updater":true,"oz_www_respect_initial_representation_on_setup":false,"oz_www_update_live_video_config_on_representation_switch":true,"oz_www_mpd_update_cancel_current_request_tracker":true,"oz_www_tagged_time_range_per_append_throttle":0,"oz_www_fix_reload_manifest_retry":true,"oz_www_fix_source_buffer_error_logging":true,"oz_www_min_block_time_range_interval_ms":0,"oz_www_fix_buffer_ahead_priority_strategy":true,"oz_www_append_last_segment_if_beyond_end":true,"oz_www_set_stream_anchor_only_on_done_status":true,"oz_www_fix_stream_deferred_cancel":true,"oz_www_sbm_abort_on_readable_stream_error":true,"oz_www_cancel_tracker_on_append_error":true,"oz_www_pixels_below_viewport_to_observe":0,"oz_www_prioritize_by_viewport_static_penalty":0,"oz_www_fix_setup_video_duration_on_representation_switch":true,"oz_www_handle_mpd_null_error_codes":true,"oz_www_fix_prepended_segments_tagging":true,"oz_www_xmlparser_use_domparser":true,"oz_www_cache_mos_threshold":false,"oz_www_load_video_node_on_unload":true,"oz_www_convert_dom_exception_to_oz_error":true,"oz_www_sbm_check_source_buffer_ready_state_on_cancel":true,"oz_www_prevent_unnecessary_seek_stream_anchor_reset":true,"oz_www_stream_types_eligible_for_partial_playback":"","oz_www_partial_playback_buffer_overflow":0.75,"oz_www_live_use_inline_manifest_after_request_manifest":false,"oz_www_live_gracefully_handle_410":true,"oz_www_segment_end_410_response":true,"oz_www_segment_end_410_response_loop_end":true,"oz_www_steadystate_minbuffer_sec":0,"oz_www_steadystate_minbuffer_buckets":0,"oz_www_steadystate_minbuffer_buckets_sec":1,"oz_www_pixels_above_viewport_to_observe":0,"oz_www_delay_stream_end_for_sourceended":true,"www_videos_shaka_use_xbox_buffered_fudge_factor":0.05,"oz_www_media_source_is_available_in_ended_state":true,"oz_www_unset_override_oz_request_implementation_on_hive_error":false,"oz_www_live_gracefully_handle_502":true,"oz_www_systemic_risk_abr_apply_representation_restrictions":true,"oz_www_dynamic_mpd_initial_playback_position_offset_modifier":4,"oz_www_player_formats_for_low_latency":"[]","oz_www_loop_body_handle_error_interval_ms":1,"oz_www_load_sequence_max_delay_ms":0,"oz_www_get_fetch_body_text_when_response_not_ok":true,"oz_www_load_sequence_only_prioritize_first_count":0,"oz_www_stream_interrupted_fuzzy_equals":false,"oz_www_sbm_skip_abort_on_media_error":true,"oz_www_sbm_abort_on_append_new_readable_stream":false,"oz_www_prevent_unnecessary_quickstarter_instance":true,"oz_www_stricter_mpd_parser_invariants":true,"oz_www_accept_external_buffering_detector":true,"oz_www_treat_inline_mpd_xml_empty_string_as_null":false,"oz_www_live_gracefully_handle_429":true,"oz_www_use_systemic_risk_abr":true,"oz_www_use_sidnee_abr":false,"oz_www_systemic_risk_abr_risk_factor":1.75,"oz_www_time_to_first_byte_ignore_above_threshold_ms":0,"oz_www_bandwidth_ttfb_samples_to_save":5,"oz_www_blue_video_player_pass_inline_mpd_xml_empty_string_as_undefined":false,"oz_www_blue_video_player_force_xhr":false,"oz_www_set_global_config":false,"oz_www_systemic_risk_abr_low_mos_risk_factor":1.3,"oz_www_systemic_risk_abr_min_watchable_mos":30,"oz_www_systemic_risk_abr_low_mos_resolution":0,"oz_www_prevent_multiple_successive_representation_switch":true,"oz_www_sidx_parser_memory_optimization":true,"oz_www_buffer_when_waiting_in_partial_buffer":true,"oz_www_stream_interrupt_in_play_buffer_overflow_target":1,"oz_www_stream_interrupt_buffer_target_timeout_ms":10000,"oz_www_recent_buffer_in_play_buffer_overflow_target":3,"oz_www_recent_buffer_timeout_ms":10000,"oz_www_use_ending_duration_for_gop_multiplier":true,"oz_www_live_trace_parse_emsg":true,"oz_www_catchup_use_timeline_range_end_time_as_end":false,"oz_www_perf_log_representation_id_more":true,"oz_www_systemic_risk_use_fetch_range_duration":false,"oz_www_use_simple_moving_average_estimator":false,"oz_www_http_no_cache":false,"oz_www_use_buffering_detector_for_playhead_interruption":true,"oz_www_enable_immediate_down_switch":false,"oz_www_handle_media_source_manager_errors":true,"oz_www_handle_invalid_webm_duration":false,"oz_www_parse_initialization_binary":true,"oz_www_systemic_risk_abr_high_estimate_confidence":52,"oz_www_data_reader_concat_once":true,"oz_www_pump_concat_once":true,"oz_www_sbm_append_readable_stream_concat_once":true,"oz_www_log_appended_segment_info":false,"oz_www_log_media_element_error_source_buffer_updateend_error":true,"oz_www_log_extra_events":true,"oz_www_overwrite_video_current_time_property":true,"oz_www_clear_buffer_on_seek_into_unbuffered_range":true,"oz_www_numerical_range_utils_is_after_range_exclusive":true,"oz_www_server_side_abr_send_client_estimates":false,"oz_www_server_side_abr_use_server_estimates":false,"oz_www_live_gracefully_handle_504":true,"oz_www_debug_live_replay":false,"oz_www_append_retry_quota_exceeded_error":true,"oz_www_buffer_target_constraint_minimum_sec":2,"oz_www_buffer_target_constraint_append_succeeded_reward":0.2,"oz_www_always_use_current_time_in_playback_state":true,"oz_www_buffer_target_constraint_quota_exceeded_penalty":0.3,"oz_www_retry_null_error_code_in_stream":false,"oz_www_emit_destroyed_after_media_keys_clear":false,"oz_www_do_not_end_stream":false,"oz_www_do_not_flush_data_queue_after_destroy":false,"oz_www_do_not_update_duration_vod":false,"oz_www_no_representation_error_detailed_message":true,"oz_www_mpd_updater_network_request_timeout_ms":20000,"oz_www_restart_media_streams_on_stream_resumed":false,"oz_www_media_stream_tracker_cancel_on_seek":false,"oz_www_media_stream_fix_double_segments_compute":false,"oz_www_fix_initial_segment_non_zero_start_time":true,"oz_www_get_mpd_least_last_time_range":true,"oz_www_respect_playback_restrictions_in_abr_fallback":true,"oz_www_null_segment_for_no_buffer_target":true,"oz_www_systemic_risk_abr_initial_risk_factor":1,"oz_www_systemic_risk_abr_document_hidden_risk_factor":0,"oz_www_use_bandwidth_estimate_from_headers_in_abr":false,"oz_www_use_ttfb_from_headers":false,"oz_www_skip_playhead_adjustment_before_initial_playback_position":false,"oz_www_clear_buffer_on_seek_epsilon_s":0,"oz_www_handle_buffered_timerange_update_on_ratechange":false,"oz_www_stub_safari_source_buffer_abort":false,"oz_www_clear_buffer_on_seek_nudge_s":0,"oz_www_streaming_task_reject_current_stream_deferred":true,"oz_www_min_buffer_behind_playhead":10,"oz_www_check_mediasource_readystate_before_end_of_stream":true,"oz_www_retry_video_element_error":true,"oz_www_clamp_seek_to_first_buffer_range_epsilon":0.75,"oz_www_ignore_restrictions_when_all_representations_restricted":false,"oz_www_hive_maximum_trimming_seconds":0,"oz_www_block_representation_status_codes_json":"[500, 503]","oz_www_block_representation_status_codes_temporarily_json":"{}","oz_www_revoke_object_url_on_detach":false,"oz_www_abort_clear_video_node_on_detach":false,"oz_www_proceed_on_representation_change_in_init_append_fail":true,"oz_www_should_check_that_source_buffer_attached":false,"oz_www_exclude_large_representations_after_restrictions":true,"oz_www_stop_loop_driver_immediately_on_cleanup":true,"oz_www_playhead_manager_handle_time_range_update_on_waiting":false,"oz_www_enable_error_recovery_attempt_logging":true,"oz_www_systemic_risk_abr_prefetch_initial_risk_factor":5,"oz_www_prefetch_resolution_threshold":810,"oz_www_throw_no_license_error":true,"oz_www_vtt_caption_representation":true,"oz_www_live_video_send_transaction_id_in_requests":false,"oz_www_log_exposure_on_oz_initialization":false,"oz_www_systemic_risk_abr_prefetch_low_mos_resolution":0,"oz_www_allow_subsequent_prefetch":true,"oz_www_comet_video_player_live_vtt_captions":false,"oz_www_prefetch_retention_duration_ms":0,"oz_www_clear_prefetch_on_unload":false,"oz_www_emit_captions_changed_event":true,"oz_www_use_vtt_captions_visible_buffer_strategy":false,"oz_www_disable_end_of_stream_in_caption_stream":true,"oz_www_append_once_per_stream_in_caption_stream":true,"oz_www_append_once_per_stream_in_application_stream":false,"oz_www_enable_era_logging_for_application_stream":false,"oz_www_emit_stream_gone_on_end_stream_before_start":true,"oz_www_sidx_segment_retry_attempts":9999,"oz_www_sidx_segment_retry_interval_ms":100,"oz_www_clear_buffer_around_playhead_boundary_ms":5000,"oz_www_fix_manifest_fetch_mixed_promise_catch":true,"oz_www_fix_update_duration_check":false,"oz_www_never_decrease_mediasource_duration":false,"oz_www_normalize_mpd_fetch_errors":true,"oz_www_no_retry_on_empty_string_error_code":false,"oz_www_use_src_object_for_media_source":false,"oz_www_mpd_parse_all_adaptation_sets":true,"oz_www_mpd_ensure_playable_representations":false,"oz_www_ull_use_broadcast_sensitivity_type":true,"oz_www_ull_fallback_stall_count":2,"oz_www_live_video_chunk_duration":0,"oz_www_av1_check_hardware_support":false,"oz_www_ultra_low_latency_bandwidth_threshold":10000000,"oz_www_bandwidth_estimate_header_key":"","oz_www_bandwidth_estimate_key":"","oz_www_fix_ull_fallback_stall_count":false,"oz_www_set_latency_context_immediately":false,"oz_www_fall_back_to_low_latency_in_ull":false,"oz_www_bandwidth_header_expire_threshold":0,"oz_www_fix_live_rewind_user_selected_playback_speed":true,"oz_www_is_csvqm_enabled":true,"oz_www_msm_refactor_wait_for_sourceopen":false}},5261],["PolarisLoggedOutDesktopBannersExposure",[],{"desktop":{"desktop_show_sign_up_as_primary_cta":false,"is_remove_duplicative_banner":true}},7958],["PolarisLoggedOutTopNavExposure",[],{"mweb":{"is_mobile_ig_logo_signup":false,"show_top_nav_sign_up_button":false,"primary_cta":"open_app","secondary_cta":"login"}},7920],["XIGSharedData",[],{"raw":"{}","native":{"www_routing_config":{"frontend_and_proxygen_routes":[{"path":"\/static\/*","destination":"DISTILLERY"},{"path":"\/config-test\/routes","destination":"DISTILLERY"},{"path":"\/*\/*\/embed","destination":"WWW"},{"path":"\/*\/*\/embed\/*","destination":"WWW"},{"path":"\/*","destination":"WWW"},{"path":"\/*\/qr","destination":"WWW"},{"path":"\/*\/collections","destination":"DISTILLERY"},{"path":"\/*\/collections\/*","destination":"DISTILLERY"},{"path":"\/*\/embed","destination":"WWW"},{"path":"\/*\/guide\/*\/*\/qr","destination":"WWW"},{"path":"\/*\/guide\/*\/*\/embed","destination":"WWW"},{"path":"\/p\/*","destination":"WWW"},{"path":"\/p\/*\/media","destination":"DISTILLERY"},{"path":"\/p\/*\/embed","destination":"WWW"},{"path":"\/p\/*\/embed\/captioned","destination":"WWW"},{"path":"\/p\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/p\/*\/false_information","destination":"WWW"},{"path":"\/*\/p\/*","destination":"WWW"},{"path":"\/*\/p\/*\/qr","destination":"WWW"},{"path":"\/*\/p\/*\/media","destination":"DISTILLERY"},{"path":"\/*\/p\/*\/embed","destination":"WWW"},{"path":"\/*\/p\/*\/embed\/captioned","destination":"WWW"},{"path":"\/*\/p\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/reel\/*","destination":"WWW"},{"path":"\/reel\/*\/media","destination":"DISTILLERY"},{"path":"\/reel\/*\/embed","destination":"WWW"},{"path":"\/reel\/*\/embed\/captioned","destination":"WWW"},{"path":"\/reel\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/reel\/*\/false_information","destination":"DISTILLERY"},{"path":"\/reel\/*\/qr","destination":"WWW"},{"path":"\/reels\/audio_page\/*","destination":"DISTILLERY"},{"path":"\/*\/reel\/*","destination":"WWW"},{"path":"\/*\/reel\/*\/qr","destination":"WWW"},{"path":"\/*\/reel\/*\/media","destination":"DISTILLERY"},{"path":"\/*\/reel\/*\/embed","destination":"WWW"},{"path":"\/*\/reel\/*\/embed\/captioned","destination":"WWW"},{"path":"\/*\/reel\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/tv\/*","destination":"WWW"},{"path":"\/tv\/*\/media","destination":"DISTILLERY"},{"path":"\/tv\/*\/embed","destination":"WWW"},{"path":"\/tv\/*\/embed\/captioned","destination":"WWW"},{"path":"\/tv\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/tv\/*\/false_information","destination":"DISTILLERY"},{"path":"\/tv\/*\/c\/*","destination":"WWW"},{"path":"\/tv\/*\/c\/*\/r\/*","destination":"WWW"},{"path":"\/tv\/configure_to_igtv","destination":"DISTILLERY"},{"path":"\/tv\/drafts","destination":"DISTILLERY"},{"path":"\/tv\/upload","destination":"DISTILLERY"},{"path":"\/tv\/upload\/*","destination":"DISTILLERY"},{"path":"\/*\/tv\/*","destination":"WWW"},{"path":"\/*\/tv\/*\/qr","destination":"WWW"},{"path":"\/*\/tv\/*\/media","destination":"DISTILLERY"},{"path":"\/*\/tv\/*\/embed","destination":"WWW"},{"path":"\/*\/tv\/*\/embed\/captioned","destination":"WWW"},{"path":"\/*\/tv\/*\/all_comments_on_ad","destination":"DISTILLERY"},{"path":"\/*\/shop","destination":"WWW"},{"path":"\/*\/shop\/all","destination":"DISTILLERY"},{"path":"\/*\/shop\/c\/*","destination":"DISTILLERY"},{"path":"\/*\/shop\/collection\/*","destination":"WWW"},{"path":"\/*\/shop\/p\/*","destination":"DISTILLERY"},{"path":"\/*\/shop2\/*","destination":"DISTILLERY"},{"path":"\/f\/*","destination":"DISTILLERY"},{"path":"\/follow\/*","destination":"DISTILLERY"},{"path":"\/*\/access_tool\/*","destination":"DISTILLERY"},{"path":"\/_n\/emaillogin","destination":"DISTILLERY"},{"path":"\/_n\/emaillogindlink","destination":"DISTILLERY"},{"path":"\/_n\/smslogin","destination":"DISTILLERY"},{"path":"\/_n\/testuserlogin","destination":"DISTILLERY"},{"path":"\/_n\/web_smslogin","destination":"DISTILLERY"},{"path":"\/_n\/web_emaillogin","destination":"DISTILLERY"},{"path":"\/_n\/nme_account_center","destination":"DISTILLERY"},{"path":"\/_n\/*","destination":"WWW"},{"path":"\/_u\/*","destination":"DISTILLERY"},{"path":"\/a\/r","destination":"WWW"},{"path":"\/about-ads","destination":"DISTILLERY"},{"path":"\/about\/*","destination":"DISTILLERY"},{"path":"\/accounts\/*","destination":"WWW"},{"path":"\/accounts\/access_tool\/*","destination":"WWW"},{"path":"\/accounts\/reauthenticate\/*","destination":"DISTILLERY"},{"path":"\/accounts\/account_recovery_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/account_recovery_send_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/account_security_info","destination":"DISTILLERY"},{"path":"\/accounts\/account_status","destination":"DISTILLERY"},{"path":"\/accounts\/activity_api_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/app_redirect_confirm_email\/*","destination":"DISTILLERY"},{"path":"\/accounts\/auth\/*","destination":"DISTILLERY"},{"path":"\/accounts\/caa_account_recovery_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/caa_ig_authentication_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/caa_ig_link_accounts_server","destination":"DISTILLERY"},{"path":"\/accounts\/caa_ig_login_retry_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/caa_login_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/caa_registration_server","destination":"DISTILLERY"},{"path":"\/accounts\/ccpa_validate","destination":"DISTILLERY"},{"path":"\/accounts\/check_email","destination":"DISTILLERY"},{"path":"\/accounts\/check_phone_number","destination":"DISTILLERY"},{"path":"\/accounts\/confirm_email\/*","destination":"WWW"},{"path":"\/accounts\/confirm_email_deeplink","destination":"WWW"},{"path":"\/accounts\/contact_point_prefill","destination":"DISTILLERY"},{"path":"\/accounts\/convert_to_professional_account","destination":"WWW"},{"path":"\/accounts\/disavow\/*","destination":"DISTILLERY"},{"path":"\/accounts\/fb_code_exchange","destination":"DISTILLERY"},{"path":"\/accounts\/fb_profile","destination":"DISTILLERY"},{"path":"\/accounts\/fx_passwordless_account_create_password_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/generate_two_factor_totp_key","destination":"DISTILLERY"},{"path":"\/accounts\/get_encrypted_credentials","destination":"DISTILLERY"},{"path":"\/accounts\/ig_badging_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_colocation_account_recovery_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_colocation_login_help_auto_send_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_colocation_password_reset_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_colocation_stop_account_deletion_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_colocation_uhl_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/ig_feo2_provider_thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/interest_topics","destination":"DISTILLERY"},{"path":"\/accounts\/ip_violation\/*","destination":"DISTILLERY"},{"path":"\/accounts\/login\/ajax","destination":"DISTILLERY"},{"path":"\/accounts\/login\/ajax\/*","destination":"DISTILLERY"},{"path":"\/accounts\/login\/ajax\/two_factor","destination":"DISTILLERY"},{"path":"\/accounts\/logout","destination":"DISTILLERY"},{"path":"\/accounts\/logout\/ajax","destination":"DISTILLERY"},{"path":"\/accounts\/logoutin","destination":"DISTILLERY"},{"path":"\/accounts\/one_click_login_error\/*","destination":"DISTILLERY"},{"path":"\/accounts\/one_tap_web_login","destination":"DISTILLERY"},{"path":"\/accounts\/password\/reset\/done","destination":"DISTILLERY"},{"path":"\/accounts\/password\/reset\/inapp","destination":"DISTILLERY"},{"path":"\/accounts\/password_reset\/*","destination":"DISTILLERY"},{"path":"\/accounts\/phone_confirm_send_sms_code","destination":"DISTILLERY"},{"path":"\/accounts\/phone_confirm_verify_sms_code","destination":"DISTILLERY"},{"path":"\/accounts\/platform_tester_invites","destination":"DISTILLERY"},{"path":"\/accounts\/process_contact_point_signals","destination":"DISTILLERY"},{"path":"\/accounts\/reclaim\/confirm","destination":"WWW"},{"path":"\/accounts\/recovery\/password_reset","destination":"DISTILLERY"},{"path":"\/accounts\/remove\/confirmed\/*","destination":"DISTILLERY"},{"path":"\/accounts\/remove\/request","destination":"DISTILLERY"},{"path":"\/accounts\/remove\/request\/permanent\/www","destination":"WWW"},{"path":"\/accounts\/remove\/request\/permanent","destination":"DISTILLERY"},{"path":"\/accounts\/remove\/request\/temporary","destination":"WWW"},{"path":"\/accounts\/remove\/request\/post\/*","destination":"DISTILLERY"},{"path":"\/accounts\/request_one_tap_login_nonce","destination":"DISTILLERY"},{"path":"\/accounts\/row_tos_confirm\/*","destination":"DISTILLERY"},{"path":"\/accounts\/safe_redirect","destination":"DISTILLERY"},{"path":"\/accounts\/send_account_recovery_email_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/send_account_recovery_sms_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/send_confirm_email","destination":"DISTILLERY"},{"path":"\/accounts\/send_signup_sms_code_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/send_two_factor_login_sms","destination":"DISTILLERY"},{"path":"\/accounts\/set_birthday","destination":"DISTILLERY"},{"path":"\/accounts\/set_comment_filter_keywords_web","destination":"DISTILLERY"},{"path":"\/accounts\/set_comment_filter_web","destination":"DISTILLERY"},{"path":"\/accounts\/set_gender","destination":"DISTILLERY"},{"path":"\/accounts\/set_presence_disabled","destination":"DISTILLERY"},{"path":"\/accounts\/set_private","destination":"DISTILLERY"},{"path":"\/accounts\/thrift_server","destination":"DISTILLERY"},{"path":"\/accounts\/two_factor_authentication\/ajax\/disable","destination":"DISTILLERY"},{"path":"\/accounts\/two_factor_authentication\/ajax\/enable","destination":"DISTILLERY"},{"path":"\/accounts\/two_factor_authentication\/ajax\/get_backup_codes","destination":"DISTILLERY"},{"path":"\/accounts\/two_factor_authentication\/disable_totp","destination":"DISTILLERY"},{"path":"\/accounts\/two_factor_authentication\/enable_totp","destination":"DISTILLERY"},{"path":"\/accounts\/username_suggestions","destination":"DISTILLERY"},{"path":"\/accounts\/validate_signup_sms_code_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/verify_email","destination":"DISTILLERY"},{"path":"\/accounts\/verify_email_link_fallback","destination":"DISTILLERY"},{"path":"\/accounts\/web_change_profile_picture","destination":"DISTILLERY"},{"path":"\/accounts\/web_create_ajax","destination":"DISTILLERY"},{"path":"\/accounts\/web_create_ajax\/attempt","destination":"DISTILLERY"},{"path":"\/accounts\/address_book\/link","destination":"DISTILLERY"},{"path":"\/accounts\/address_book\/unlink","destination":"DISTILLERY"},{"path":"\/accounts\/remove\/request\/post\/temporary","destination":"DISTILLERY"},{"path":"\/hacked","destination":"WWW"},{"path":"\/support\/chat\/embed\/ig","destination":"WWW"},{"path":"\/accounts_center\/home","destination":"WWW"},{"path":"\/accounts_center\/profiles","destination":"WWW"},{"path":"\/accounts_center\/service","destination":"WWW"},{"path":"\/acredirect","destination":"DISTILLERY"},{"path":"\/ads\/*","destination":"WWW"},{"path":"\/ads\/ad_report_update","destination":"WWW"},{"path":"\/ads_data_from_partners\/ac_3pd_redirect","destination":"DISTILLERY"},{"path":"\/ads_data_from_partners\/legacy_3pd_redirect","destination":"DISTILLERY"},{"path":"\/ads_data_from_partners\/standalone_3pd_redirect","destination":"DISTILLERY"},{"path":"\/ads_data_from_partners\/standalone_legacy_3pd_redirect","destination":"DISTILLERY"},{"path":"\/ads_data_from_partners\/ac_3pd_linked_business_redirect","destination":"DISTILLERY"},{"path":"\/ajax\/bz","destination":"WWW"},{"path":"\/api\/*","destination":"DISTILLERY"},{"path":"\/api\/v1\/direct_v2\/restoration_cdn_url","destination":"WWW"},{"path":"\/api\/graphql","destination":"WWW"},{"path":"\/app\/hyperlapse","destination":"DISTILLERY"},{"path":"\/ar\/*","destination":"WWW"},{"path":"\/ar\/*\/push","destination":"DISTILLERY"},{"path":"\/assisted_account_recovery\/*\/*\/*","destination":"DISTILLERY"},{"path":"\/authenticity\/authenticity\/location\/get_location_verification","destination":"DISTILLERY"},{"path":"\/authenticity\/authenticity\/location\/set_location_verification","destination":"DISTILLERY"},{"path":"\/badges_milestones_management","destination":"DISTILLERY"},{"path":"\/bfad3e85bc","destination":"DISTILLERY"},{"path":"\/bfad3e85bc_cheap","destination":"DISTILLERY"},{"path":"\/business\/*","destination":"DISTILLERY"},{"path":"\/categories\/accounts","destination":"DISTILLERY"},{"path":"\/categories\/accounts\/*","destination":"DISTILLERY"},{"path":"\/challenge","destination":"WWW"},{"path":"\/challenge\/*","destination":"WWW"},{"path":"\/challenge\/action","destination":"WWW"},{"path":"\/challenge\/action\/*","destination":"WWW"},{"path":"\/challenge\/replay","destination":"DISTILLERY"},{"path":"\/challenge\/replay\/*","destination":"DISTILLERY"},{"path":"\/challenge\/reset","destination":"DISTILLERY"},{"path":"\/challenge\/reset\/*","destination":"DISTILLERY"},{"path":"\/challenge\/rewind","destination":"DISTILLERY"},{"path":"\/challenge\/rewind\/*","destination":"DISTILLERY"},{"path":"\/challenge\/stateless_challenge_with_node_id","destination":"DISTILLERY"},{"path":"\/client_error","destination":"DISTILLERY"},{"path":"\/coming_soon\/*","destination":"DISTILLERY"},{"path":"\/componentexplorer\/embeds","destination":"DISTILLERY"},{"path":"\/componentexplorer\/react","destination":"DISTILLERY"},{"path":"\/concurrent_request","destination":"DISTILLERY"},{"path":"\/persistent_request","destination":"DISTILLERY"},{"path":"\/api\/internal\/persistent_request_thrift","destination":"DISTILLERY"},{"path":"\/create\/configure_to_story","destination":"DISTILLERY"},{"path":"\/create\/configure","destination":"DISTILLERY"},{"path":"\/dogfoodnow\/whitelist_add","destination":"DISTILLERY"},{"path":"\/dogfoodnow\/whitelist_remove","destination":"DISTILLERY"},{"path":"\/live\/*\/comment\/*\/flag","destination":"DISTILLERY"},{"path":"\/media\/*\/comment\/*\/flag","destination":"DISTILLERY"},{"path":"\/live\/*\/flag","destination":"DISTILLERY"},{"path":"\/media\/*\/flag","destination":"DISTILLERY"},{"path":"\/users\/*\/flag","destination":"DISTILLERY"},{"path":"\/users\/*\/report","destination":"DISTILLERY"},{"path":"\/data\/experiments","destination":"DISTILLERY"},{"path":"\/data\/manifest.json","destination":"WWW"},{"path":"\/data\/qe_params","destination":"DISTILLERY"},{"path":"\/data\/shared_data","destination":"DISTILLERY"},{"path":"\/developer\/*","destination":"DISTILLERY"},{"path":"\/digital_collectibles","destination":"WWW"},{"path":"\/direct_v2\/*","destination":"DISTILLERY"},{"path":"\/directory\/hashtags","destination":"DISTILLERY"},{"path":"\/directory\/hashtags\/*","destination":"DISTILLERY"},{"path":"\/directory\/hashtags\/*","destination":"DISTILLERY"},{"path":"\/directory\/profiles","destination":"DISTILLERY"},{"path":"\/directory\/profiles\/*","destination":"DISTILLERY"},{"path":"\/directory\/profiles\/*","destination":"DISTILLERY"},{"path":"\/directory\/suggested\/*","destination":"DISTILLERY"},{"path":"\/donate\/redirect","destination":"DISTILLERY"},{"path":"\/donate\/ecp_checkout","destination":"DISTILLERY"},{"path":"\/donate\/ecp_redirect","destination":"DISTILLERY"},{"path":"\/download\/request","destination":"WWW"},{"path":"\/download\/request_download_data_ajax","destination":"DISTILLERY"},{"path":"\/download\/confirm\/*","destination":"WWW"},{"path":"\/download","destination":"WWW"},{"path":"\/download\/file.php","destination":"WWW"},{"path":"\/p\/dl\/download\/file.php","destination":"WWW"},{"path":"\/dyi\/download\/auth\/*","destination":"DISTILLERY"},{"path":"\/dyi\/lookaside_auth","destination":"DISTILLERY"},{"path":"\/emails\/emails_sent","destination":"WWW"},{"path":"\/emails\/preferences","destination":"DISTILLERY"},{"path":"\/emails\/unsubscribe\/*","destination":"WWW"},{"path":"\/embed_logger","destination":"DISTILLERY"},{"path":"\/enoozer\/action\/current\/*\/*\/*","destination":"WWW"},{"path":"\/enoozer\/action\/snooze","destination":"DISTILLERY"},{"path":"\/enoozer\/action\/undo","destination":"DISTILLERY"},{"path":"\/errors\/403","destination":"DISTILLERY"},{"path":"\/errors\/404","destination":"DISTILLERY"},{"path":"\/errors\/500","destination":"DISTILLERY"},{"path":"\/facebook_pay\/*","destination":"DISTILLERY"},{"path":"\/facebook_pay\/connect_learn_more","destination":"DISTILLERY"},{"path":"\/fb\/connect\/ajax","destination":"DISTILLERY"},{"path":"\/fb\/create\/ajax","destination":"DISTILLERY"},{"path":"\/fb\/create\/ajax\/attempt","destination":"DISTILLERY"},{"path":"\/fbsurvey","destination":"DISTILLERY"},{"path":"\/fbsurvey\/confirm_user","destination":"WWW"},{"path":"\/fxcal\/*","destination":"WWW"},{"path":"\/fxcal\/auth","destination":"WWW"},{"path":"\/fxcal\/auth\/login","destination":"WWW"},{"path":"\/fxcal\/auth\/login\/ajax","destination":"DISTILLERY"},{"path":"\/fxcal\/auth\/two_fac_login\/ajax","destination":"DISTILLERY"},{"path":"\/fxcal\/get_linking_auth_blob","destination":"DISTILLERY"},{"path":"\/fxcal\/iab_settings_confirm_login","destination":"WWW"},{"path":"\/fxcal\/iab_settings_perform_login","destination":"DISTILLERY"},{"path":"\/fxcal\/ig_sso_login","destination":"DISTILLERY"},{"path":"\/fxcal\/ig_sso_users","destination":"DISTILLERY"},{"path":"\/fxcal\/passwordless\/confirm_password\/ajax","destination":"DISTILLERY"},{"path":"\/fxcal\/reauth","destination":"WWW"},{"path":"\/fxcal\/reauth_login","destination":"WWW"},{"path":"\/fxcal\/specific_login","destination":"WWW"},{"path":"\/getapp","destination":"DISTILLERY"},{"path":"\/graphql\/ig_schema","destination":"DISTILLERY"},{"path":"\/graphql\/query","destination":"DISTILLERY"},{"path":"\/graphql_www","destination":"WWW"},{"path":"\/help\/*\/*","destination":"DISTILLERY"},{"path":"\/help\/ipreporting\/*","destination":"WWW"},{"path":"\/help\/support\/*","destination":"WWW"},{"path":"\/igtv\/configure_to_igtv","destination":"DISTILLERY"},{"path":"\/igtv\/drafts","destination":"DISTILLERY"},{"path":"\/igtv\/upload","destination":"DISTILLERY"},{"path":"\/igtv\/upload\/*","destination":"DISTILLERY"},{"path":"\/igtv_revshare_onboarding","destination":"DISTILLERY"},{"path":"\/instagramstickers","destination":"DISTILLERY"},{"path":"\/intern\/*","destination":"DISTILLERY"},{"path":"\/internal\/ig_product_principles","destination":"DISTILLERY"},{"path":"\/interstitial\/covid19\/*","destination":"DISTILLERY"},{"path":"\/invites\/contact","destination":"DISTILLERY"},{"path":"\/legal\/*","destination":"WWW"},{"path":"\/legal\/privacy","destination":"WWW"},{"path":"\/legal\/terms\/usgov","destination":"DISTILLERY"},{"path":"\/linking\/*","destination":"WWW"},{"path":"\/linking\/active_promotions","destination":"DISTILLERY"},{"path":"\/linking\/business_conversion","destination":"DISTILLERY"},{"path":"\/linking\/create_post","destination":"DISTILLERY"},{"path":"\/linking\/create_story","destination":"DISTILLERY"},{"path":"\/linking\/edit_profile","destination":"DISTILLERY"},{"path":"\/linking\/enter_promotion_payment","destination":"DISTILLERY"},{"path":"\/linking\/follow_and_invite_friends","destination":"DISTILLERY"},{"path":"\/linking\/igtv_upload","destination":"DISTILLERY"},{"path":"\/linking\/insights","destination":"DISTILLERY"},{"path":"\/linking\/inspiration_hub","destination":"DISTILLERY"},{"path":"\/linking\/news","destination":"DISTILLERY"},{"path":"\/linking\/promote","destination":"DISTILLERY"},{"path":"\/linking\/promote_story","destination":"DISTILLERY"},{"path":"\/linking\/use_appointments","destination":"DISTILLERY"},{"path":"\/linking\/view_all_lead_opportunities","destination":"DISTILLERY"},{"path":"\/linking\/view_all_leads","destination":"DISTILLERY"},{"path":"\/linking\/view_lead_details","destination":"DISTILLERY"},{"path":"\/linking\/view_lead_opportunity_details","destination":"DISTILLERY"},{"path":"\/linking\/view_services","destination":"DISTILLERY"},{"path":"\/linkshim\/*","destination":"DISTILLERY"},{"path":"\/linkshim","destination":"DISTILLERY"},{"path":"\/local\/dev\/transaction_tool_selector\/redirect\/save_fbe_extras","destination":"DISTILLERY"},{"path":"\/local\/dev\/transaction_tool_selector\/redirect","destination":"WWW"},{"path":"\/location_search","destination":"DISTILLERY"},{"path":"\/explore\/grid","destination":"DISTILLERY"},{"path":"\/explore\/map","destination":"DISTILLERY"},{"path":"\/explore\/locations","destination":"WWW"},{"path":"\/explore\/locations\/*","destination":"WWW"},{"path":"\/explore\/locations\/*\/qr","destination":"WWW"},{"path":"\/explore\/search\/keyword\/*","destination":"WWW"},{"path":"\/logging\/falco","destination":"DISTILLERY"},{"path":"\/media\/*\/comment\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/*\/appeal_ridge","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/*\/dismiss_ridge","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/*\/dispute_ridge","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/*\/restore_ridge","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/*\/ridge_info","destination":"WWW"},{"path":"\/media\/*\/copyright\/am_info","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/appeal","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/delete","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/dismiss_am","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/dispute_am","destination":"DISTILLERY"},{"path":"\/media\/*\/copyright\/done","destination":"WWW"},{"path":"\/media\/*\/copyright\/info","destination":"DISTILLERY"},{"path":"\/media\/*\/delete","destination":"DISTILLERY"},{"path":"\/media\/*\/edit","destination":"DISTILLERY"},{"path":"\/media\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/media\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/media\/*\/product\/*\/flag","destination":"DISTILLERY"},{"path":"\/media\/*\/product\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/media\/*\/*\/story_poll_vote","destination":"DISTILLERY"},{"path":"\/media\/*\/*\/story_slider_vote","destination":"DISTILLERY"},{"path":"\/mixi\/oauth_callback","destination":"DISTILLERY"},{"path":"\/oauth\/*","destination":"WWW"},{"path":"\/oauth\/accept_platform_tester_invite","destination":"DISTILLERY"},{"path":"\/oauth\/access_token","destination":"DISTILLERY"},{"path":"\/oauth\/authorize","destination":"DISTILLERY"},{"path":"\/oauth\/batch_revoke_platform_permissions","destination":"DISTILLERY"},{"path":"\/oauth\/decline_platform_tester_invite","destination":"DISTILLERY"},{"path":"\/oauth\/info_sharing_disclaimer","destination":"WWW"},{"path":"\/oauth\/ofa_control\/allowlist_app","destination":"DISTILLERY"},{"path":"\/oauth\/ofa_control\/turn_on_future_activity","destination":"DISTILLERY"},{"path":"\/oauth\/ofa_control\/unblocklist_app","destination":"DISTILLERY"},{"path":"\/oauth\/oidc\/user_consent","destination":"DISTILLERY"},{"path":"\/oauth\/oidc\/user_reject","destination":"DISTILLERY"},{"path":"\/oauth\/platform_tester_invites","destination":"DISTILLERY"},{"path":"\/oauth\/revoke_access","destination":"DISTILLERY"},{"path":"\/oauth\/thrift_server","destination":"DISTILLERY"},{"path":"\/oidc\/state","destination":"WWW"},{"path":"\/qp\/batch_fetch_web","destination":"DISTILLERY"},{"path":"\/n\/*","destination":"DISTILLERY"},{"path":"\/nametag","destination":"DISTILLERY"},{"path":"\/p-ng","destination":"DISTILLERY"},{"path":"\/payments\/paypal_close","destination":"WWW"},{"path":"\/press","destination":"WWW"},{"path":"\/privacy\/activity_center","destination":"DISTILLERY"},{"path":"\/privacy\/checks","destination":"WWW"},{"path":"\/publicapi\/*","destination":"DISTILLERY"},{"path":"\/push\/preferences","destination":"DISTILLERY"},{"path":"\/push\/removetoken","destination":"DISTILLERY"},{"path":"\/push\/web\/get_push_info","destination":"DISTILLERY"},{"path":"\/push\/web\/register","destination":"DISTILLERY"},{"path":"\/push\/web\/update_settings","destination":"DISTILLERY"},{"path":"\/qr","destination":"WWW"},{"path":"\/raters\/summary\/*","destination":"DISTILLERY"},{"path":"\/realtime\/*","destination":"DISTILLERY"},{"path":"\/reports\/*\/flag\/hacked","destination":"DISTILLERY"},{"path":"\/reports\/*\/flag\/hacked\/*","destination":"DISTILLERY"},{"path":"\/reports\/flag_hacked_user","destination":"DISTILLERY"},{"path":"\/reports\/submit_reporter_appeal","destination":"DISTILLERY"},{"path":"\/reports\/user_report_support_feedback","destination":"DISTILLERY"},{"path":"\/reports\/web\/get_frx_prompt","destination":"DISTILLERY"},{"path":"\/reports\/web\/handle_guided_action","destination":"DISTILLERY"},{"path":"\/reports\/web\/notify_guardian","destination":"DISTILLERY"},{"path":"\/reports\/web\/log_tag_selected","destination":"DISTILLERY"},{"path":"\/repute\/*","destination":"DISTILLERY"},{"path":"\/restriction\/*","destination":"DISTILLERY"},{"path":"\/s\/*","destination":"WWW"},{"path":"\/security\/*","destination":"DISTILLERY"},{"path":"\/sem\/campaign","destination":"WWW"},{"path":"\/sem\/campaign\/login","destination":"WWW"},{"path":"\/sem\/campaign\/emailsignup","destination":"WWW"},{"path":"\/seo\/google_widget\/crawler","destination":"DISTILLERY"},{"path":"\/session\/login_activity\/avow_login","destination":"DISTILLERY"},{"path":"\/session\/login_activity\/disavow_login_activity","destination":"DISTILLERY"},{"path":"\/session\/login_activity\/logout_session","destination":"DISTILLERY"},{"path":"\/session\/login_activity\/undo_avow_login","destination":"DISTILLERY"},{"path":"\/settings\/help\/monetization_status","destination":"WWW"},{"path":"\/shop\/cart","destination":"DISTILLERY"},{"path":"\/shop\/cart\/*","destination":"DISTILLERY"},{"path":"\/shop\/products\/*","destination":"WWW"},{"path":"\/shopping\/bag","destination":"DISTILLERY"},{"path":"\/shopping\/home","destination":"DISTILLERY"},{"path":"\/sitemap\/*","destination":"DISTILLERY"},{"path":"\/stories\/*","destination":"WWW"},{"path":"\/suggested_users\/remove_from_suggested\/*\/*","destination":"DISTILLERY"},{"path":"\/suggested_users\/remove_from_suggested_confirm","destination":"DISTILLERY"},{"path":"\/support","destination":"DISTILLERY"},{"path":"\/survey\/us2020\/consent_withdraw","destination":"DISTILLERY"},{"path":"\/switcher\/placeholder","destination":"DISTILLERY"},{"path":"\/tags\/*","destination":"DISTILLERY"},{"path":"\/tags\/*\/qr","destination":"DISTILLERY"},{"path":"\/terms\/*","destination":"WWW"},{"path":"\/terms\/unblock","destination":"WWW"},{"path":"\/terms\/start\/*","destination":"DISTILLERY"},{"path":"\/test_users\/*","destination":"DISTILLERY"},{"path":"\/testing\/indigo_logging","destination":"DISTILLERY"},{"path":"\/testing\/validate_client_input","destination":"DISTILLERY"},{"path":"\/thirdparty\/static\/*","destination":"DISTILLERY"},{"path":"\/topics\/*","destination":"DISTILLERY"},{"path":"\/topics\/*\/*","destination":"DISTILLERY"},{"path":"\/two_factor\/*","destination":"DISTILLERY"},{"path":"\/two_factor\/two_factor_login","destination":"WWW"},{"path":"\/uid\/*","destination":"WWW"},{"path":"\/users\/*\/flag\/options","destination":"DISTILLERY"},{"path":"\/users\/*\/report_celebrity","destination":"DISTILLERY"},{"path":"\/users\/*\/report_underage","destination":"DISTILLERY"},{"path":"\/users\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/users\/*\/report\/*","destination":"DISTILLERY"},{"path":"\/users\/merchant\/*\/product\/*\/flag","destination":"DISTILLERY"},{"path":"\/users\/merchant\/*\/product\/*\/flag\/*","destination":"DISTILLERY"},{"path":"\/users\/self","destination":"WWW"},{"path":"\/users\/set_disallow_story_reshare_web","destination":"DISTILLERY"},{"path":"\/users\/set_feed_post_reshare_disabled_web","destination":"DISTILLERY"},{"path":"\/verification\/request","destination":"WWW"},{"path":"\/verification\/request\/done","destination":"WWW"},{"path":"\/verification\/*","destination":"DISTILLERY"},{"path":"\/video_call\/*","destination":"DISTILLERY"},{"path":"\/votinginfocenter","destination":"DISTILLERY"},{"path":"\/watch_together\/*","destination":"DISTILLERY"},{"path":"\/web\/lite","destination":"WWW"},{"path":"\/web\/*","destination":"DISTILLERY"},{"path":"\/webinstall\/instagram","destination":"DISTILLERY"},{"path":"\/webinstall\/instagram\/loggedin","destination":"DISTILLERY"},{"path":"\/webinstall\/instagram\/loggedout","destination":"DISTILLERY"},{"path":"\/webinstall\/iglite","destination":"DISTILLERY"},{"path":"\/webinstall\/iglite\/loggedin","destination":"DISTILLERY"},{"path":"\/webinstall\/iglite\/loggedout","destination":"DISTILLERY"},{"path":"\/weibo\/oauth_callback","destination":"DISTILLERY"},{"path":"\/wifiauth\/login\/*","destination":"DISTILLERY"},{"path":"\/xwoiynko","destination":"DISTILLERY"},{"path":"\/zr\/diagnostics","destination":"DISTILLERY"},{"path":"\/zr\/nux\/update_preference","destination":"DISTILLERY"},{"path":"\/explore","destination":"WWW"},{"path":"\/e2e\/*","destination":"DISTILLERY"}],"frontend_only_routes":[{"path":"\/create\/(story|style|details|location|tag|advanced-settings|advanced-settings\/alt-text)\/?","destination":"BOTH"},{"path":"\/accounts\/login\/two_factor\/?","destination":"BOTH"},{"path":"\/explore\/search\/?","destination":"BOTH"},{"path":"\/direct\/t\/?.*","destination":"BOTH"},{"path":"\/stories\/?.*","destination":"BOTH"},{"path":"\/[^\/]+\/similar_accounts\/?","destination":"BOTH"},{"path":"\/[^\/]+\/related_profiles\/?","destination":"BOTH"},{"path":"\/[^\/]+\/(following|hashtag_following|followers)\/?(mutualOnly|mutualFirst)?\/?","destination":"BOTH"}],"proxygen_request_handler_only_routes":[{"paths":["^\/$","^$"],"destination":"WWW","in_vpn_dogfooding":false,"in_qe":false}]},"platform_install_badge_links":{"ios":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/Yfc020c87j0.png","android":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/c5Rp7Ym-Klz.png","windows_nt_10":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yu\/r\/EHY6QnZYdNX.png"}}},6186],["CometUrlTransformsConfig",[],{"should_remove_trailing_slash":false},6589],["CometRouterConfig",[],{"bulkRouteFetchBatchSize":5},7542],["PolarisLoggedOutHomePageSSRQE",[],{"is_home_page_ssr":false},7940],["MqttWebDeviceID",[],{"clientID":"a918e3c1-d094-4cea-8758-e8695ed1a4c9"},5003],["cr:1468",[],{"__rc":[null,null]},-1],["cr:804",["CometColumn.react"],{"__rc":["CometColumn.react",null]},-1],["cr:842",[],{"__rc":[null,null]},-1],["cr:1005",[],{"__rc":[null,null]},-1],["cr:1027",[],{"__rc":[null,null]},-1],["cr:1088",[],{"__rc":[null,null]},-1],["cr:1183",["LSStaticDependencies"],{"__rc":["LSStaticDependencies",null]},-1],["cr:1329",["MWXButtonImpl.react"],{"__rc":["MWXButtonImpl.react",null]},-1],["cr:1350",["CometColumnItem.react"],{"__rc":["CometColumnItem.react",null]},-1],["cr:2004",[],{"__rc":[null,null]},-1],["cr:2075",[],{"__rc":[null,null]},-1],["cr:2151",[],{"__rc":[null,null]},-1],["cr:2203",[],{"__rc":[null,null]},-1],["cr:2536",["GetLsDatabaseForComet"],{"__rc":["GetLsDatabaseForComet",null]},-1],["cr:2667",["CometRow.react"],{"__rc":["CometRow.react",null]},-1],["cr:2922",[],{"__rc":[null,null]},-1],["cr:3083",["CometRowItem.react"],{"__rc":["CometRowItem.react",null]},-1],["cr:3084",[],{"__rc":[null,null]},-1],["cr:3411",[],{"__rc":[null,null]},-1],["cr:3752",[],{"__rc":[null,null]},-1],["cr:4067",["FDSGlimmer.react"],{"__rc":["FDSGlimmer.react",null]},-1],["cr:4790",[],{"__rc":[null,null]},-1],["cr:4853",[],{"__rc":[null,null]},-1],["cr:6669",["DataStore"],{"__rc":["DataStore",null]},-1],["cr:6725",["FDSTextPairing.react"],{"__rc":["FDSTextPairing.react",null]},-1],["cr:8709",[],{"__rc":[null,null]},-1],["cr:10191",[],{"__rc":[null,null]},-1],["cr:757",["ImageWwwCssDependency"],{"__rc":["ImageWwwCssDependency",null]},-1],["cr:9378",[],{"__rc":[null,null]},-1],["cr:9379",[],{"__rc":[null,null]},-1],["cr:2306",["NullState404FailedLoadingFB"],{"__rc":["NullState404FailedLoadingFB",null]},-1],["cr:3211",["NullStateGeneralFB"],{"__rc":["NullStateGeneralFB",null]},-1],["cr:3587",["NullStatePermissionsFB"],{"__rc":["NullStatePermissionsFB",null]},-1],["cr:10677",[],{"__rc":[null,null]},-1],["cr:983844",[],{"__rc":[null,null]},-1],["cr:1072546",[],{"__rc":[null,null]},-1],["cr:1072547",[],{"__rc":[null,null]},-1],["cr:1072549",[],{"__rc":[null,null]},-1],["cr:5278",["ReactDOM-prod.classic"],{"__rc":["ReactDOM-prod.classic",null]},-1],["cr:1172",["WebSession"],{"__rc":["WebSession",null]},-1],["cr:6016",["NavigationMetricsWWW"],{"__rc":["NavigationMetricsWWW",null]},-1],["cr:7384",["cancelIdleCallbackWWW"],{"__rc":["cancelIdleCallbackWWW",null]},-1],["cr:9985",["performanceAbsoluteNow"],{"__rc":["performanceAbsoluteNow",null]},-1],["cr:9986",["CurrentUser"],{"__rc":["CurrentUser",null]},-1],["cr:1268629",["setTimeoutCometLoggingPri"],{"__rc":["setTimeoutCometLoggingPri",null]},-1],["cr:1268630",["setTimeoutCometSpeculative"],{"__rc":["setTimeoutCometSpeculative",null]},-1],["cr:1396",["cometAsyncFetch"],{"__rc":["cometAsyncFetch",null]},-1],["cr:3019",[],{"__rc":[null,null]},-1],["cr:3579",[],{"__rc":[null,null]},-1],["cr:3697",[],{"__rc":[null,null]},-1],["cr:4022",[],{"__rc":[null,null]},-1],["cr:8496",[],{"__rc":[null,null]},-1],["cr:9804",[],{"__rc":[null,null]},-1],["cr:10676",[],{"__rc":[null,null]},-1],["cr:6873",[],{"__rc":[null,null]},-1],["cr:927622",[],{"__rc":[null,null]},-1],["cr:927623",[],{"__rc":[null,null]},-1],["cr:1473549",[],{"__rc":[null,null]},-1],["cr:1836099",[],{"__rc":[null,null]},-1],["cr:1871597",[],{"__rc":[null,null]},-1],["cr:1947728",[],{"__rc":[null,null]},-1],["cr:1993377",[],{"__rc":[null,null]},-1],["cr:921407",["useNoopDebuggingInfoComponent"],{"__rc":["useNoopDebuggingInfoComponent",null]},-1],["cr:1634616",["CometUserActivity"],{"__rc":["CometUserActivity",null]},-1],["cr:1708227",["useThrottledComet"],{"__rc":["useThrottledComet",null]},-1],["cr:1816",[],{"__rc":[null,null]},-1],["cr:1801726",[],{"__rc":[null,null]},-1],["cr:13733",[],{"__rc":[null,null]},-1],["LSPlatformMessengerSyncParams",[],{"mailbox":"{\"bloks_version\":\"a89ee435e154006f88d132e2332db31f89e2bd22497acd8ed81f4796f0f33da1\",\"full_height\":200,\"locale\":\"en_US\",\"preview_height\":360,\"preview_height_large\":360,\"preview_width\":480,\"preview_width_large\":480,\"scale\":1,\"snapshot_num_threads_per_page\":15}","contact":"{\"locale\":\"en_US\"}","e2ee":"{\"locale\":\"en_US\"}","communityExclusive":"{\"locale\":\"en_US\"}"},6729],["LSPlatformWorkplaceSyncParams",[],{"mailbox":"{\"full_height\":200,\"locale\":\"en_US\",\"preview_height\":200,\"preview_height_large\":400,\"preview_width\":183,\"preview_width_large\":360,\"scale\":1,\"snapshot_num_threads_per_page\":15}","contact":"{\"locale\":\"en_US\"}","e2ee":"{\"locale\":\"en_US\"}"},6730],["MessengerMSplitFlag",[],{"is_msplit_account":false,"is_dma_consent_declined":false},7414],["IntlBCP47LocaleConfig",[],{"bcp47Locale":"en-US"},4448],["BanzaiConfig",[],{"MAX_SIZE":10000,"MAX_WAIT":150000,"MIN_WAIT":null,"RESTORE_WAIT":150000,"blacklist":["time_spent"],"disabled":false,"gks":{"boosted_pagelikes":true,"platform_oauth_client_events":true,"sticker_search_ranking":true},"known_routes":["artillery_javascript_actions","artillery_javascript_trace","artillery_logger_data","logger","falco","gk2_exposure","js_error_logging","loom_trace","marauder","perfx_custom_logger_endpoint","qex","require_cond_exposure_logging","metaconfig_exposure"],"should_drop_unknown_routes":true,"should_log_unknown_routes":false},7],["cr:2625",[],{"__rc":[null,null]},-1],["cr:5028",[],{"__rc":[null,null]},-1],["cr:8931",["FDSProgressRingIndeterminate.react"],{"__rc":["FDSProgressRingIndeterminate.react",null]},-1],["cr:10054",["FDSButton.react"],{"__rc":["FDSButton.react",null]},-1],["cr:5695",["EventListenerWWW"],{"__rc":["EventListenerWWW",null]},-1],["cr:8909",["ReactFiberErrorDialogWWW"],{"__rc":["ReactFiberErrorDialogWWW",null]},-1],["cr:692209",["cancelIdleCallbackComet"],{"__rc":["cancelIdleCallbackComet",null]},-1],["DataStoreConfig",[],{"expandoKey":"__FB_STORE","useExpando":true},2915],["cr:1353359",["CometEventListener"],{"__rc":["CometEventListener",null]},-1],["WebConnectionClassServerGuess",[],{"connectionClass":"EXCELLENT"},4705],["FbtQTOverrides",[],{"overrides":{"1_000b6572c6a3f7fe9312e5879dd2e75b":"You'll need to log in with Workplace to continue this video chat.","1_023ac1e3f0ce2980598584f26a784b9f":"Ignore messages","1_028dc427119e6bfbfcd5eb2dd83b2a9e":"View Page status","1_075684469438a60ae5f6813949e94a0d":"Get quote","1_0778dc4cf3fe167942881fecddd5dee2":"Learn more","1_08a3224cc0fd966f2bb0e780c51e6a0b":"New poll","1_0a090165a1d0654210eb444114aabd7c":"Switch between accounts","1_0b9af3d5b6a4de6cb2b17ad5a0beec3a":"Learn more","1_0d0b40d72cd2adc492a402e98e18896f":"Chat notifications","1_0ea6e742163878d88375800514788740":"Invite link","1_0ea7de82b669cced737b30875f15309a":"Local event from Facebook","1_0f008d2991187964d472eceaf9ba28d6":"Featured sticker packs","1_0f48efb82ce58bf43dec6a98dcadc874":"Add your new number","1_0f9fceeb2e66627d9e346dd24e0d6916":"Remove from channel","1_0fee0283487e0259495a07f9e315ad8f":"Your home in Messenger","1_1068c1352d8cbb8919cc2b4a0dbcd9f3":"Hide contact","1_10811a6ece4ca15b10dc22f89805a347":"Customize your chat with {short-name}.","1_117fb24f8ee951759e9435520cc71e70":"More options","1_1248a8548f1b43fd3d9fc77baf835a04":"Contacts only","1_1593f9d2cc4c63f196a61a70eff664cf":"Send to group","1_159aa796a642d08a85379ec9693d25c7":"Opt in","1_1736f6743cf12be3ffc46cd556357e96":"Book now","1_18b8ec487f180574ad865f168eeafa70":"Content not found","1_1905e45a72593e291dda8c774aa4caf4":"Invite people","1_1ac128eda299351dc18567e7a6f31be6":"Hide video","1_1b38f249fefb0fd5ef7912a1fe615d10":"Pinned location","1_1b59f7e84dba4c8754cf60d1bafa6ae5":"Shared stories","1_1cf36465e606a10ef2a48c5dee532085":"Great job, {user_name}!","1_1d36c6e7b1a07971c84821452f9dc407":"Ignore conversation","1_1d89beed629123cabeeea834c345a7fc":"Mute notifications","1_1f209b12cabbe35509c514220825d53b":"New story","1_209b3fb19e7c487ffe3bd85b2adac6db":"Try again","1_20cec0b4386ad8555f8b619ad2c2fb81":"Single pop","1_215afaeceab4d29970af2c11221f79e3":"Web visibility","1_226d5171b148e60fe004a4f3cc53a81b":"Delete group chat?","1_23f5a1596d301feaeb32b47f24dc73a0":"Join call","1_243d55bab0d83c72b2113bfd5ca2e194":"Membership questions","1_25589d7cb1db33911bf18252dbb5155c":"Message history in Inbox","1_2745ba03fa7b9c0f59c0797fb44da204":"Showing in chats","1_27f38b56fa58a394e2d89fbf7288747b":"New sender","1_28ea9e6140b5437477564e5b21353246":"Profile picture","1_2953f6f20942da4f0593b905a4db3d90":"See details","1_2af4c8cb4d30a1aaa744a75187d6b06d":"{number} invited by you","1_2b2898b200686215c54616553499fddf":"Unread messages","1_2b406f4727fff3df7dd970cac1c41536":"Messenger preview","1_2c2ff60e8d5edccadadf61f739b6d87b":"Report story","1_3002f3a3232973642407c2e3830c10f6":"Stop sharing location","1_30ed561a77bfcadb3b66d5960c2a9e05":"Photo reminders","1_313c1c8a5025b45c60712685f0d89c6c":"App visibility","1_33886f5d4a6ede055ec28ddf69251cc5":"Life events","1_3543833b8b31fbb1561d46f2c0b266a8":"Added with theme","1_366d38e456780d92844ab4b39ac1de78":"Not interested","1_37900af383a573c0337521bca05d7955":"Respond to event","1_37ebfbfd36c55a8366f7ba9d528cf7b3":"Chats you can create","1_39339bb4b3f3002e589625a820bf5c7a":"Learn more","1_3a9a1e192465754ec4427995fe1cffb4":"Buy and sell groups","1_3aa3f2c2971602310d482c632c086db8":"Chat hosts","1_3bc7a4f74be5e3dbfdc9b758fa779fff":"Chat plugin","1_3cee79cd9e136ffc84ccfc7082bef6c2":"{number_of_happening_now_events} happening now, {number_of_upcoming_events} upcoming","1_3e8fba90f69e371d19c5b4f79e3f0be7":"Buy and sell groups","1_3f4c233aac1d71d17bee559b932144d3":"See conversation","1_41446ff5d2de26a67626d2ba309c969b":"This video can't be sent","1_4151657ef8e7bc03ab8169e5dcb0d675":"Cancel request","1_41eadd6427237386cc04b60a8ab94a8b":"This chat will show as unread","1_46793f5529ff4a62f831cf9218082b7f":"Unread requests","1_46879d905028aaee9f7297d27c075b50":"See messages","1_46b9f298de3c041a464dbe8ff7f3d978":"Language settings","1_46c8d595559f4232c4a7fe113aac3093":"Get started","1_475781e5e945e3d217b563d6ccd51ecd":"Create prompt","1_489630491bec0288ae7c0bef88ff5ad9":"Show music picker","1_4ad1c9e7de7af0b7d1853ed6863469db":"Snooze for {number_of_hours} hours","1_4b56df30045efb8a5d21ec865d43ec1c":"Approved by you","1_4b9736a9d6cbeb6249b0704870ec383e":"No devices yet","1_4cf8fe13a0639e31c0d73b5aec3b8019":"Something went wrong","1_4d5c8cbda9ac3dfc82b483ecf952a53c":"Open Facebook app","1_4e75a018ef44c107750832d736fcce90":"Send details","1_4ee7496edd4dafc3c2b2a6225f1a6f69":"Nearby places","1_5009586cb3b7953608b1ccc56cb3e630":"See conversation?","1_50c0e7742a3eb3800f3c2fdd5bce8f3a":"Admins & moderators","1_531aa532255f18fbb4386d4ac4bf537d":"Search emoji","1_53e9c4c2a53662ab23979d6cd79d4417":"Stop sharing","1_5490d986c6908e35ac70ae79cca740fc":"Switch account","1_55c0717e522433cf319a51f6ed6d4d09":"No messages","1_55c2f7ac43fba60f684a0a0dfd01bb89":"Edit avatar","1_55e31911698e89d3b19d4c703079cdf2":"{content} Learn more","1_56d2098fc23416108de3ceae0fd6c158":"See link","1_599d20d959e0009397c73fb9edb426dd":"Private post","1_5af9abe8c5f4d9bcce27117d09ca6932":"Start call","1_5e86ca443695bbd6605bcd169ee35a74":"Group updates","1_5f0a4852946206863aa44a9ec3f87708":"Send to group","1_60a7a58934bd27cbaf2058b53ff745f6":"Leave game","1_60eb52f4ce4a109523fbfa8e90244331":"Live location","1_617aeb029449c78895903ece88034b31":"Show picture-in-picture","1_61cb9f934ffb6b5f8cc4cb95757125d4":"Invalid time","1_6315107c7594ac961c8dac9aabbb957a":"Add to her picture","1_64b1b9a14a334d3cce48f22f2b03e7c2":"Not now","1_6544e705bd98780c45018863ca564aa1":"Block messages","1_6582285731ad9288ac97889beeca82f3":"Avatar settings","1_66402d631b18879269b46a49f95a0a4e":"Noise suppression","1_6689492f38a51b5cb39982dd8a0e7f00":"Account details","1_678bfb1d36a580695ccbb699c8fd1bd2":"Logging in\u2026","1_6795cc13b37b3be61a143c35c9c65382":"Recently shared","1_6a9a0529abd169ff91b49b4022dbf5a5":"Buy and sell groups","1_6b124b9a53cd1299ad43ceef50dcd0e2":"Unread chats","1_6d2f04c835bd2e9e555649e2f121fd5f":"Introducing AI stickers","1_6dc5cc58c44e3791e14cdb69816e8a3f":"Product catalog terms","1_6ec9c14f5b6103937c24960c6ae37947":"SMS messages","1_7008293f762c6b49632496bd6aad21ff":"Suggested chats","1_70190249ea4fa344ffbe77fd48af796f":"Pause chat?","1_701d063f9d93574540e7a4aa27d2f86d":"Message reactions","1_7052e2f38bec805609d7986562d34ed0":"Your reactions","1_72920428a45b969c9dad788a656c323c":"Skip to details and actions","1_7341e8b3089e0af586ed3b9682c2b5cf":"View call","1_73761caf2fde503928bfdbd48c983136":"See conversation?","1_7808c5327cf430807c173fa11ac0cc26":"Learn more","1_7930f1b92ced21f16265c1ab07265964":"Chats you can join","1_7bf132b7beb84dbc96f9cc6a1caef3a3":"Last name","1_7c5789ad7c9455a96fa0b8d3edaf1dd0":"View profile","1_7e3e738782f1887fbcebca5e62902a72":"See group","1_7f626e74849fb5ad4a61825532fb6054":"Confirm your identity","1_801af62106d995c8b376a512e2146039":"Block messages","1_806d0518a4e1e599c196185438e2b79c":"Change image","1_83a0754dbad2db42dcbe0e8900e6b48a":"AI-assisted message","1_84698e2e6128e955605ddff2615c2771":"In transit","1_88b60e4824d116c36468b700b6287e2f":"Your location","1_8a1749bf031ab122983b76b370a86be3":"Learn more","1_8bc33223ef4caf9b437b812c2772d946":"Create poll","1_8c84ed97d7d84a31c72b1c75300a9461":"Delete chat?","1_8dea727922641bc0de681cb214274b2f":"View AR object","1_8e82c5b24398a0887342f439b66ce8c3":"{user}'s location","1_8ea29d4da797ad3ae8fa2b3626b2a50c":"Disabled chat","1_8ef9ffb962319c095470bb46de00beaa":"Current location","1_9025bb6bcf560d6de6cfd22af6eaec97":"All chats menu","1_9050fb0878cf1e782d24779cf780114c":"Recent calls","1_91d783db2fb886ee4801ae5e0a86e04c":"Channel admin","1_92255cd3d8f183d6dcb03b606a3445c2":"Recent searches","1_925dfeb7269a4b97e5035aede422151c":"Upload contacts","1_92b1a4d18dca5da9ac47d17733885fc2":"How to add friends on Messenger Kids","1_93183c880d14f092e5d9617d9a246a74":"Read receipts","1_939fba302a75b306e132ccb37e09a148":"Ignore group","1_944401d1748eeaa9a66e62241477695e":"View details","1_948415d2b551fa7c8b50376738732e5b":"Community members","1_9626d7ac31beaf24bbd48f4842bf4744":"{num_activities} activities","1_9645bee1f9dba4ee355d68df18cb1102":"Contact card","1_96cc0d1d8acdfbcc9fe4623a53183f99":"No more posts","1_983e4f9e7f9ecfdb8a2d0aa8247942de":"More conversations","1_99dd31ad1b3145dfb03b7b4b097f28d5":"Send current location","1_9ed1ff8f2501b81918e505f6e17fd362":"Send separately","1_a1195adc52046789d21a0ae117244224":"Creating poll","1_a12b852de26a50e5b6986edc7fa2705e":"Account created","1_a2ed1fddb5b17414f3b7941385713361":"Suggested people","1_a3d27f40032c3217f0934bcd46d52392":"Learn more","1_a3f05430c2d2c4a7949a503649a0941d":"4 things to know about your information","1_a4694c6ccbc990026015c70c944fe25e":"Cover photo","1_a64a04c8ea9a8cf38124918e78c71b60":"You've blocked this account.","1_a7a430455b6aaba0be1cf776314c8e70":"Learn more","1_a7e141af65d2cd2dc972d3c094d2ce4f":"Charge your Bluetooth keys regularly.","1_a8fd7153d9fbad9cece5913d6268813c":"Voice and video calling","1_a96a641ba1f4b43910fab6d1b55c9b17":"Not now","1_a9c08e1b18c1bceb358a7bf4a1aee0aa":"View profile","1_ab80b68f0048ce8515584d069d120405":"Submit a report","1_abd30739736c002c9a49c782066cbe86":"Save changes","1_add682c72addd3a0d8b6fcab3720aadc":"Turn on","1_aeb4b99dd7b73001a4f730b4a9120e04":"Try again","1_aec2472fe4a2eaccb817d6111a4c0d39":"Video call","1_af9c98d11efedfee4f1301601a67874a":"Double knock","1_afc0eae78aa06ac4e92bf98ac3a03177":"Ignore group","1_b0308bd1c93ff21594fabd353bda0a2a":"Red\/green","1_b14ffeb649c54cac70fe09d9f7780889":"Open sticker, emoji and GIF keyboard.","1_b22b6c4a8dd3ff71f35d007751cd87b0":"Get the Messenger app","1_b2cea7ff1ee86133589fc73e5f2f3f9d":"You can turn this off at any time in your Parent Dashboard.","1_b32cee1f96ea285d99c5ca73d4eb725f":"Date of birth","1_b3dd269103f0d9b89d9bdb677dbd8887":"Invalid link","1_b3ecf06a63fd5147cac3c083201ac7eb":"Data saver","1_b42224e77c208d4ee532f212f5fe7a47":"Learn more","1_b449f7098ace13c92ffc9bb9d5a5bb6f":"Live description (optional)","1_b45945f81d03ceaf6f9441f2eeeec891":"Contact us","1_b4c7d1e15b39ef2c3956027bb4d6cd11":"Placed on {date} \u00b7 Canceled","1_b6392edec7f022a20e9867eb0b24de7b":"Featured Facebook photos","1_b6f50b519cec90102cc5b62361a81288":"Mute notifications","1_b81d470fc8105e7a7896e7cffb0ceeed":"Add contact","1_b9143060878dce3a509e6bc2548b82f2":"Search for adults","1_b997548b5fdd3a2dee73c3392135d911":"{number} invited by you","1_ba4838bc3349d125cfb867715cada2f9":"Update build","1_bbc5d4c00b66cc87bd1e6f8ab51fc102":"Call with video","1_bbd9c674819da6d44ca09fa575180083":"Videos to send","1_bc1a68f2efbc9ac36f13fe05f5d65e51":"Unmute notifications","1_bc5ed53c58ed1544e3e014e9d7dee341":"Creating community","1_bf841bb55b37d0620ef1b2bea096b95f":"You waved at {$recipient}.","1_bfbf4cbd94a30fe78e2c6243fbaedb73":"No internet connection","1_bfef0efc933e18bc735d53351af694e0":"Photos to send","1_c174849dd6b0df72ce6c611bda774209":"Add option","1_c21bf170fea995d887a6b64c13639323":"Double pop","1_c6f4d12c2c30c1986800afdd50f373cd":"Preview chat","1_c794c37e69d7f325e9a433f02ba8790b":"Message requests","1_c8077b6c0597db47a0485bc0f32e9980":"Your avatar","1_c921177d0d05ed9c9b95487f15422056":"Delete channel","1_c94482ebd9b72b746183c50a4d4208d6":"Send a like","1_cb73b265ac209451363883bed772c9bb":"Play together","1_cc78ccf039dccf8d1dea818b85eab80d":"More people","1_cd6b327676433f7b3c3515f206c0b82a":"{phone_number} \u2022 Phone contact","1_cdc01fc97f5a6cf6ba07c7bcc4fe11e1":"Add question","1_cde9138094eb836637af973172431d53":"{name1} wants to add {lastPendingKifTargetName} as a friend","1_ce3d72055f43aaf90d886ab0017ca08c":"Recent articles","1_cef77356ede0b83cf0465641b0719a42":"Problem with Bluetooth or audio source","1_d29c32cd116f7833d1f496f064788d8c":"Report someone in this chat","1_d3e1e228c31890a4aba20db8d31fd323":"Notification control","1_d7bbd024b73557f1cf0914a38113498d":"Block messages","1_d8de8ea2ef707a7aace4a752b147d8f1":"Mark as read","1_d9f5379b09800045f33f218dc5408f64":"Welcome message","1_dbd60e7eb18c870f9603d90f44f244ab":"Group chats","1_dc6a01243c06b93a27cbe6c6d6c795f3":"Camera roll","1_dcabb4806e92c408bd735494ddd92a6c":"Draw a necklace","1_dee291c2ba2b66491a65be6138906278":"Hidden group","1_df45795d00cab7a89a5557f9a392a7b2":"You opened this chat from {Origin Domain}","1_df57e221cb0b224e5a0090f7dcef6677":"Forward limit reached","1_df848a5c2d023027ac455f8321243645":"Report buyer","1_e11f9f6dcd24ac5786c0eb8ff1851e1b":"Update information?","1_e12cc3ec2ab93b6916804e5e1f6a336f":"Add to story","1_e146ca287d980280ff6dabc5d32b2713":"Leave conversation?","1_e250ac43039a943db6bd1855c02f6c39":"Learn more","1_e27604669dde9743f8c4a735e650e5a6":"Thanks for being a superstar in this chat!","1_e57e0918dc3eb089646890b6bb915dc0":"Choose kids for {name1}, {name2} and {name count} others to chat with","1_e7861583dd9505c6c9a5dd36aca38d3b":"Unblock messages","1_e809c2825e3b050976f7ca22f1532032":"Faster messaging","1_e8d7d977b19c2aa1894496a663c986dc":"Blue\/yellow","1_ea8ff502404e09cf262e602989d843d8":"Go to recent chats","1_eceb9aa9398269f52436f1a1a7ee41b4":"Send to group","1_ee9abb17ff7ad017ae988a02f8f5beae":"Top friends","1_eec0e983014426e06f0c4077e7333275":"Unblock messages?","1_ef4b4300b7a1f0319566068f5568c938":"Updating poll","1_f165e0191456b0373edec046de3290d5":"Active now","1_f2010c43a90ee7c3b7d6d3cab66ef06e":"Social networking","1_f33ba2aba991e0820ccfef1ac81c4c14":"Recommended communities","1_f580546da084946da3d6f61e3cc636da":"Channel settings","1_f5d924ee511bdbc00c3dd05a10fe8260":"Please update your app","1_f816fc32554f392be8655ee6db8f7dd5":"Continue with PayPal","1_f90fb65f92ad8ac33f140b8be3c9eed1":"Invalid file format","1_f919ada00521135434fd084a87e64542":"Add photos","1_fa663c0ee32eeae58fd133765c35f905":"Learn more","1_fba7ed548a73364cce9a2ad6e168b798":"Audio call","1_fd3afb0fabe31263a19dac9f61fb0d4f":"Look up info","1_fd7ada49a7f6f2ab82454ec27b9c6725":"Couldn't remove message","1_fdf2eec743eaf4ee4b25a683f71525c6":"Add a profile picture","1_fe84ad51b794fd555ef027662cbb6f2e":"Event creation","1_ff1c542ee2c5bb59ee27ade5e7e52cb4":"Resume chats","1_ff6b115a8a131f9f1b4b8c9c80ec38d4":"For families","1_01f3beddbcd5491063fd7587b0785fd7":"Video call is full","1_0bb657a03fcace8be70ff1c092b35b62":"Forward limit reached","1_12213f25a4e94520a59b51c3d565edf1":"Date of birth","1_12d2f816058bda88b9f56cd9dedd5e16":"You can turn this off at any time in your Parent Dashboard.","1_138bdbd67af3b9c9d4cdd6c8cc8708a2":"Something went wrong","1_183c5e431133dafadb40a2627c25432d":"Something's wrong","1_1d0e61a46a120591d790f382f91f9c99":"Search for adults","1_23423f479668dd06c46036d8f37edf9a":"You opened this chat from {Origin Domain}","1_2882107be72d37df895b05e0573990a6":"Unblock messages?","1_2b99f4eb0d10fb2a590c91881bc93620":"Can't create duplicate account","1_2dfe4f010e821ffa89d9c4133df72506":"Problem with Bluetooth or audio source","1_2e510be3887bae0dd4d73733dffa6ec4":"{firstname1}, {firstname2} and {firstname3} are online","1_2e9405cf33a60079eba08f7433c66595":"Unable to connect your call","1_2feb150fdf16fe534e635fa1143a7508":"No devices yet","1_31426213a5556e794c313d21dbeceb6b":"Turn on","1_3bce071d20c866b7a915136c01c2a17f":"Remove from channel","1_3e252ef60c34f4c269a640aedaaf9a91":"Recent calls","1_4323029c4a9559df6910965a98e5f7ee":"Customize your chat with {short-name}.","1_441e2b39aa374ab1c0a622f1b6e7fc94":"Delete {Account Name}'s Messenger Kids account","1_4994c24b8d4dddbbef04db4ac77b8bcc":"Link privacy","1_49d7f78ceda6bf498723e714f09ab445":"Thanks for being a superstar in this chat!","1_5281d15cd9033cfae6f25bbfb95f2a84":"Add people","1_55329bd3473ce3a2df9059b20779a464":"My preview","1_55d8b31242e3dd713affa2d9c303ca18":"Open camera settings menu","1_57095066a5af6f37f159d7a7bcc474b4":"Settings > Add a new device","1_572460b539878eceb021da6f0ba4ad27":"Search in conversation","1_588c13ae9c29e4cad57ff0623473419b":"You can't message this group","1_59e4d389c284d58c6a7006f6b0562811":"Photo reminders","1_5a928f58cc901c2f036154287466ac52":"AI-assisted message","1_5af74bfaddbfdad2526294491119b1ac":"Call notifications","1_5b15ff8357f34309683c127f15840806":"Chat plugin","1_5b4abffee3f305389a50710f7d485df8":"Introducing AI stickers","1_5db467841ea204babe44c0bb4275c013":"Message sent","1_5dbfacce1399e2f5cb7a6380beba0d09":"This may take a moment...","1_67045d680945154400cf43a419e4ff9b":"Please update your app","1_6831028223b7159f4a16a762309aeda5":"Invite with a link","1_68667c64d4e948188b48f13c7b8324c0":"Open Facebook app","1_69b4fbe4cc084fbff4fcf68d2f9d5fed":"Charge your Bluetooth keys regularly.","1_6cd66115c7adbd7d8d450aaa8a5015d9":"Contact phone number","1_74630a0214a11fa5068b0655daafe2de":"Facebook user","1_752a4442f6eb09a9c79427caefd174c5":"4 things to know about your information","1_76f971d0cf5dc8a40e43aa6592bf6015":"{num_activities} activities","1_79cb46c945b63a0e9c8bd77518fd38f0":"Add a profile picture","1_7a4acef2efaf880fb516077a756bea63":"Image ID not returned.","1_8842669d806d74dfddee6ca515f2d8db":"Look up info","1_8c442034b2e9c1bcc394eb6907b1d510":"You'll need to log in with Workplace to continue this video chat.","1_92469592130a0e85a5e996fa12296099":"Account details","1_9dbc389a7657e7c498fe3cd2e8c903b9":"Messenger preview","1_a02773f3eb15c1a027aacc96b97f526f":"Invalid link","1_a2a5724aff5a7cca2d69a4e30e754431":"Open System Preferences, then use the Security & Privacy controls to grant Workplace Chat access to the camera and microphone. You may need to restart the app for the settings to be applied.","1_aa3f089a4051c1e5ed29b0473ecb462c":"Last name","1_aa408b4afeb14b97cfcc9915842b0b00":"Confirm your identity","1_aa542254b59b7d679b6fdb93590402d3":"Show picture-in-picture","1_b2fcaaf664421e83c164b2db208e6c2b":"Dance party","1_bac81400de6624c1e42b308333c30946":"Move to grid","1_beebce762afa64542777f56c09d80178":"How to add friends on Messenger Kids","1_c0a187c52a5eda47cc616d9da9d48264":"This lets you play Apple Music tracks, add tracks to your music library and more.","1_c0bf6a1e1c71e84db2d85d977ea0352c":"You asked to chat with {name1}.","1_c470052944bf04f16919dc993025b1f3":"Send separately","1_c65fba5a9f5914e3837eddb303417521":"Placed on {date} \u00b7 Canceled","1_c70d03f023ad810666fb2b89f962b34f":"Blue\/yellow","1_c995f1a5f939c822beefb3ff278bf59c":"Thanks for being a superstar in this chat!","1_cd4fecb8aead25c0effd4ac632f98755":"Cut over this chat to E2EE","1_d138dd68d66991762d0960a448c9dbf1":"Chat members","1_d2306e12f55ac0dad00099ff94a9ecce":"Install Workplace Chat audio sharing software?","1_d2abcef26824c99a9b7b66ebfd5b36a8":"Manage folders","1_d47fd6e064b6d1afcb5e5d3674dff1e2":"Start chatting","1_d757e02fa81fb3f8bb93135f107c6516":"Test speaker","1_db1d708e4daa1d62b46287504eae8acc":"This video can't be sent","1_df5b853bbb16433e2ce5f781de7d297b":"Squad hang","1_e4cad6566ee6fa7548d52ba7824506c0":"Remove address","1_e67d8767d558befbca8c04d5b99d91d9":"Draw a necklace","1_e811fc5925e4dcd78c046551cb24e889":"This chat will show as unread","1_ed32b0c87ff94ede7145d9b17ba192ba":"View {remainingCount} more photos","1_ee45d6f16d585bfe6cebb6800c4822d4":"Add to her picture","1_f5bffa9a6c448e63ca61155760dca5fd":"No messages","1_f6ffd6ae71c283f8aec7c7bc2cbfa289":"Open audio device selector menu","1_fc4f960e763c5eedcbf5c27cf0411d4b":"{name1} wants to add {lastPendingKifTargetName} as a friend","1_fe319617901612fc3c881e2d69ade21d":"Choose kids for {name1}, {name2} and {name count} others to chat with"}},551],["CountryNamesConfig",[],{"codeToName":{"AD":"Andorra","AE":"United Arab Emirates","AF":"Afghanistan","AG":"Antigua and Barbuda","AI":"Anguilla","AL":"Albania","AM":"Armenia","AN":"Netherlands Antilles","AO":"Angola","AQ":"Antarctica","AR":"Argentina","AS":"American Samoa","AT":"Austria","AU":"Australia","AW":"Aruba","AX":"Aland Islands (Finland)","AZ":"Azerbaijan","BA":"Bosnia & Herzegovina","BB":"Barbados","BD":"Bangladesh","BE":"Belgium","BF":"Burkina Faso","BG":"Bulgaria","BH":"Bahrain","BI":"Burundi","BJ":"Benin","BL":"Saint Barthelemy","BM":"Bermuda","BN":"Brunei","BO":"Bolivia","BQ":"Bonaire, Sint Eustatius and Saba","BR":"Brazil","BS":"The Bahamas","BT":"Bhutan","BV":"Bouvet Island","BW":"Botswana","BY":"Belarus","BZ":"Belize","CA":"Canada","CC":"Cocos (Keeling) Islands","CD":"Democratic Republic of the Congo","CF":"Central African Republic","CG":"Republic of the Congo","CH":"Switzerland","CI":"Ivory Coast","CK":"Cook Islands","CL":"Chile","CM":"Cameroon","CN":"China","CO":"Colombia","CR":"Costa Rica","CU":"Cuba","CV":"Cape Verde","CW":"Curacao","CX":"Christmas Island","CY":"Cyprus","CZ":"Czech Republic","DE":"Germany","DJ":"Djibouti","DK":"Denmark","DM":"Dominica","DO":"Dominican Republic","DZ":"Algeria","EC":"Ecuador","EE":"Estonia","EG":"Egypt","EH":"Western Sahara","ER":"Eritrea","ES":"Spain","ET":"Ethiopia","FI":"Finland","FJ":"Fiji","FK":"Falkland Islands","FM":"Federated States of Micronesia","FO":"Faroe Islands","FR":"France","GA":"Gabon","GB":"United Kingdom","GD":"Grenada","GE":"Georgia","GF":"French Guiana","GG":"Guernsey","GH":"Ghana","GI":"Gibraltar","GL":"Greenland","GM":"Gambia","GN":"Guinea","GP":"Guadeloupe","GQ":"Equatorial Guinea","GR":"Greece","GS":"South Georgia and the South Sandwich Islands","GT":"Guatemala","GU":"Guam","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong","HM":"Heard Island and McDonald Islands","HN":"Honduras","HR":"Croatia","HT":"Haiti","HU":"Hungary","ID":"Indonesia","IE":"Ireland","IL":"Israel","IM":"Isle of Man","IN":"India","IO":"British Indian Ocean Territory","IQ":"Iraq","IR":"Iran","IS":"Iceland","IT":"Italy","JE":"Jersey","JM":"Jamaica","JO":"Jordan","JP":"Japan","KE":"Kenya","KG":"Kyrgyzstan","KH":"Cambodia","KI":"Kiribati","KM":"Comoros","KN":"Saint Kitts and Nevis","KP":"North Korea (DPRK)","KR":"South Korea","KW":"Kuwait","KY":"Cayman Islands","KZ":"Kazakhstan","LA":"Laos","LB":"Lebanon","LC":"Saint Lucia","LI":"Liechtenstein","LK":"Sri Lanka","LR":"Liberia","LS":"Lesotho","LT":"Lithuania","LU":"Luxembourg","LV":"Latvia","LY":"Libya","MA":"Morocco","MC":"Monaco","MD":"Moldova","ME":"Montenegro","MF":"Saint Martin","MG":"Madagascar","MH":"Marshall Islands","MK":"Macedonia","ML":"Mali","MM":"Myanmar","MN":"Mongolia","MO":"Macau","MP":"Northern Mariana Islands","MQ":"Martinique","MR":"Mauritania","MS":"Montserrat","MT":"Malta","MU":"Mauritius","MV":"Maldives","MW":"Malawi","MX":"Mexico","MY":"Malaysia","MZ":"Mozambique","NA":"Namibia","NC":"New Caledonia","NE":"Niger","NF":"Norfolk Island","NG":"Nigeria","NI":"Nicaragua","NL":"Netherlands","NO":"Norway","NP":"Nepal","NR":"Nauru","NU":"Niue","NZ":"New Zealand","OM":"Oman","PA":"Panama","PE":"Peru","PF":"French Polynesia","PG":"Papua New Guinea","PH":"Philippines","PK":"Pakistan","PL":"Poland","PM":"Saint Pierre and Miquelon","PN":"Pitcairn Islands","PR":"Puerto Rico","PS":"Palestine","PT":"Portugal","PW":"Palau","PY":"Paraguay","QA":"Qatar","RE":"Reunion","RO":"Romania","RS":"Serbia","RU":"Russia","RW":"Rwanda","SA":"Saudi Arabia","SB":"Solomon Islands","SC":"Seychelles","SD":"Sudan","SE":"Sweden","SG":"Singapore","SH":"St. Helena","SI":"Slovenia","SJ":"Svalbard and Jan Mayen","SK":"Slovakia","SL":"Sierra Leone","SM":"San Marino","SN":"Senegal","SO":"Somalia","SR":"Suriname","SS":"South Sudan","ST":"Sao Tome and Principe","SV":"El Salvador","SX":"Sint Maarten","SY":"Syria","SZ":"Swaziland","TC":"Turks and Caicos Islands","TD":"Chad","TF":"French Southern and Antarctic Lands","TG":"Togo","TH":"Thailand","TJ":"Tajikistan","TK":"Tokelau","TL":"Timor-Leste","TM":"Turkmenistan","TN":"Tunisia","TO":"Tonga","TR":"Turkey","TT":"Trinidad and Tobago","TV":"Tuvalu","TW":"Taiwan","TZ":"Tanzania","UA":"Ukraine","UG":"Uganda","UM":"United States Minor Outlying Islands","US":"United States of America","UY":"Uruguay","UZ":"Uzbekistan","VA":"Vatican City","VC":"Saint Vincent and the Grenadines","VE":"Venezuela","VG":"British Virgin Islands","VI":"United States Virgin Islands","VN":"Vietnam","VU":"Vanuatu","WF":"Wallis and Futuna","WS":"Samoa","XK":"Kosovo","YE":"Yemen","YT":"Mayotte","ZA":"South Africa","ZM":"Zambia","ZW":"Zimbabwe"}},3614],["PolarisSiteData",[],{"country_code":"US","device_id":"CD5B157A-74AB-4D01-BA90-0939AF84E3DB","machine_id":"Z9r1pwAEAAHnoBnAsKqFNV5SgWX7","send_device_id_header":true,"e2e_config":null,"use_server_machine_id":false},7369],["PolarisIgLiteLoggedOutUpsellsExposure",[],{"global_4gb_and_above_enabled":false},7928],["BarcelonaCrossMetaCampaignTokens",[],{"mweb_app_header":"AQGzadJhz-2hjU9pln-2AfiTYx2m5-4W5NIpu9x_DallSv0","ig_desktop_nav":"AQGzMHLrsCM5AVXiTk3PGocEPTMIo_f1IDJPL7wBgpPxCtw","ig_mobile_nav":"AQGztqP3v75XzUN7loFlWrZhH-h3MXzG9vDAe8ISrIBjTRo","ig_web_joiner_badge_to_profile":"AQGzATkUarP_ZfsD9qUr-nVFFPq5okXlpqls4ZE_k-ozIF0","ig_web_joiner_badge_with_notif":"AQGz0ND_2033D06J4WOdu8UXbw_ToH8sOLcXc7Np34Vjw4w","seo":"AQGzHicoo5aiC-uOiipNSpl3eUfOz5TEYJOMEUNgZq6rUBU","invite":"AQGzS5SsUM24OEq2PPYu26LWxAXfz2tWH70gcWUq_20DtIA","invite_on_threads_as":"AQGzr-XKQFk12ExTWq_DKbCXV7kzmIn2_AbVUvvBhcOTpg0","invite_lets_connect":"AQGzcRqlrJPRaSTdNWXenSsFOXTu0qbchoZ7LgGH4wGK5SM","invite_join_me":"AQGzDSnzEjOAH0fGvU_z87Dk-0EXv5KgQwePZb7M0OBYpas","copy_link":"AQGz6LfwnPuCKbbTHgkzcF3smMWHNN552dhCxbRh4S46Sg"},7651],["AnalyticsCoreData",[],{"device_id":"$^|AcYohY-RmMVI3RhrGykcMQDHqHUwo8tCzCM6PSRCXN6gmsdnoY_XX7e718IN75F97Ooj1u4-mMJZPrdEpdDMdHRvQpT5|CD5B157A-74AB-4D01-BA90-0939AF84E3DB","app_id":"936619743392459","enable_bladerunner":false,"enable_ack":true,"push_phase":"C3","enable_observer":false,"enable_cmcd_observer":false,"enable_dataloss_timer":false,"enable_fallback_for_br":true,"queue_activation_experiment":false,"max_delay_br_queue":60000,"max_delay_br_queue_immediate":3,"max_delay_br_init_not_complete":3000,"consents":{},"app_universe":2,"br_stateful_migration_on":false,"enable_non_fb_br_stateless_by_default":false,"use_falco_as_mutex_key":false,"is_intern":false,"enable_session_id_bug_fix":true,"identity":{"appScopedIdentity":"","claim":"hmac.AR0m9T1G_cOAe6p04Jqf3OFJz0TII0vOOBnQ3JFM8GGtri6M"},"app_version":"1.0.0"},5237],["WebDevicePerfClassData",[],{"deviceLevel":"unknown","yearClass":null},3665]],"instances":[["__inst_af5d41d9_0_0_Dj",["XIGCometRoutedRootConfigBuilder"],[{"service_worker_uri":"\/www-service-worker.js?s=push&__d=www","pwa_share_service_worker_uri":"\/www-service-worker.js?s=push&pwa_share=1","ssr_enabled":false}],1]],"require":[["CometPrelude"],["JSScheduler"],["ServerJS"],["ScheduledApplyEach"],["CometResourceScheduler"],["PolarisPostRoot.entrypoint"],["PolarisPostRoot.react"],["CometPlatformRootClient","initialize",["__inst_af5d41d9_0_0_Dj","RequireDeferredReference"],[{"ConfigOrBuilder":{"__m":"__inst_af5d41d9_0_0_Dj"},"backgroundRouteInfo":{"route":null,"timeSpentMetaData":null},"additional_roots":[],"client_id":"7483563829649516416","rootElementID":"mount_0_0_aR","expectedPreloaders":null,"initialRouteInfo":{"route":{"actorID":"0","rootView":{"allResources":[{"__dr":"PolarisPostRoot.entrypoint"},{"__dr":"PolarisPostRoot.react"}],"resource":{"__dr":"PolarisPostRoot.react"},"props":{"media_id":"3590436264358116880","media_owner_id":"402833667","media_type":8,"page_logging":{"name":"postPage","params":{"page_id":"postPage_3590436264358116880","owner_id":"402833667"}},"polaris_preload":{"media":{"expose":false,"preloaderID":"6503355136063058699","request":{"method":"GET","url":"\/api\/v1\/media\/{media_id}\/info\/","params":{"path":{"media_id":"3590436264358116880"}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false},"comments":{"expose":false,"preloaderID":"7229756575182900066","request":{"method":"GET","url":"\/api\/v1\/media\/{media_id}\/comments\/","params":{"path":{"media_id":"3590436264358116880"},"query":{"can_support_threading":true,"target_comment_id":null,"permalink_enabled":false}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false},"related_media":{"expose":false,"preloaderID":"630730386603591517","request":{"method":"GET","url":"\/api\/v1\/feed\/user\/{user_id}\/","params":{"path":{"user_id":"402833667"},"query":{"count":7}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false}},"qr":false,"enable_seo_crawling_pool":false,"should_fetch_related_users_async":true,"should_increase_comment_previews":false,"should_include_audio_in_chaining_posts":false,"accessibility_caption":null,"custom_en_creator_activation_upsell_header_text":null,"custom_en_creator_activation_upsell_body_text":null,"specified_country":"US","specified_language":"en","should_chain_with_trending_content":false,"should_enable_ios_global_sound":false,"is_in_skip_deeplink_experiment":false},"entryPoint":{"__dr":"PolarisPostRoot.entrypoint"}},"tracePolicy":"polaris.postPage","meta":{"title":"CAPTURE THE ATLAS | \u2728 A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! \ud83d\udd34\ud83d\udc9a\n\nLast Thursday night, under the freezing skies of Northern Alaska, I... | Instagram","accessory":null,"favicon":null},"prefetchable":true,"canonicalRouteName":"comet.igweb.PolarisPostRoute","entityKeyConfig":{"entity_type":{"source":"constant","value":"post"},"entity_id":{"source":"param","value":"shortcode"}},"hostableView":{"allResources":[{"__dr":"PolarisPostRoot.entrypoint"},{"__dr":"PolarisPostRoot.react"}],"resource":{"__dr":"PolarisPostRoot.react"},"props":{"media_id":"3590436264358116880","media_owner_id":"402833667","media_type":8,"page_logging":{"name":"postPage","params":{"page_id":"postPage_3590436264358116880","owner_id":"402833667"}},"polaris_preload":{"media":{"expose":false,"preloaderID":"6503355136063058699","request":{"method":"GET","url":"\/api\/v1\/media\/{media_id}\/info\/","params":{"path":{"media_id":"3590436264358116880"}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false},"comments":{"expose":false,"preloaderID":"7229756575182900066","request":{"method":"GET","url":"\/api\/v1\/media\/{media_id}\/comments\/","params":{"path":{"media_id":"3590436264358116880"},"query":{"can_support_threading":true,"target_comment_id":null,"permalink_enabled":false}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false},"related_media":{"expose":false,"preloaderID":"630730386603591517","request":{"method":"GET","url":"\/api\/v1\/feed\/user\/{user_id}\/","params":{"path":{"user_id":"402833667"},"query":{"count":7}}},"preloadEnabledOnInit":false,"preloadEnabledOnNav":false}},"qr":false,"enable_seo_crawling_pool":false,"should_fetch_related_users_async":true,"should_increase_comment_previews":false,"should_include_audio_in_chaining_posts":false,"accessibility_caption":null,"custom_en_creator_activation_upsell_header_text":null,"custom_en_creator_activation_upsell_body_text":null,"specified_country":"US","specified_language":"en","should_chain_with_trending_content":false,"should_enable_ios_global_sound":false,"is_in_skip_deeplink_experiment":false},"entryPoint":{"__dr":"PolarisPostRoot.entrypoint"}},"polarisRouteConfig":{"hideBottomNavigation":false,"mobileHeaderVariant":"post_title","shellQeValues":{"skipOpenAppDeeplinkAttempt":null},"pageID":"postPage"},"url":"\/p\/DHTysJtvv4Q\/","params":{"permalink_key":"p","shortcode":"DHTysJtvv4Q","view":null,"caption":null,"q":null,"chaining":false,"username":null,"sharing_info":null,"img_index":null,"viewer":false,"show_insights":false,"igshid":null,"igsh":null,"utm_source":null,"utm_medium":null,"gclid":null,"campaign_id":null,"partner_id":null,"tracking_id":null,"extra_1":null,"extra_2":null,"event_time":null,"sem_attr_id":null,"placement":null,"creative":null,"keyword":null,"dl_source":null,"l":null},"routePath":"\/{permalink_key}\/{shortcode}\/"},"timeSpentMetaData":{"container_id":"DHTysJtvv4Q"}},"qplEvents":{"initial_load":{"r":1,"i":553648129},"navigation":{"r":1,"i":553648130}},"ssrEnabled":false}]],["Bootloader","markComponentsAsImmediate",[],[["CometKeyCommandWrapperDialog.react","CometModifiedKeyCommandWrapperDialog.react","IGDSPro2ProDialog.react","PolarisCoauthorReviewDialog.react","VideoPlayerHTML5ApiCea608State","VideoPlayerHTML5ApiWebVttState","PolarisPostFastGenAITransparencyModal.react","PolarisPostFastGenAITransparencyModal.next.react","PolarisBoostMediaIneligibleErrorDialog.react","IGWebBloksApp","PolarisPostFastAdDebugToolModal.react","PolarisPostFastGenAITransparencyZeroClickModal.next.react","PolarisCreateCollectionWithPostModal.react","PolarisPostFastAboutThisAccountModal.react","PolarisPostFastCopyModal.react","PolarisPostFastDeleteModal.react","PolarisPostFastEmbedModal.react","PolarisPostFastHideAdModal.react","PolarisPostFastOptionsModal.react","PolarisPostFastReportAdModal.react","PolarisPostFastReportModal.react","PolarisPostFastShareSheet.react","PolarisPostFastTaggedModal.react","PolarisPostFastUnfollowModal.react","PolarisPostFastWAISTModal.react","PolarisPostFavoritesModal.react","PolarisBoostMusicErrorDialog.react","PolarisPostFastAdRemovedModal.react","PolarisPostFastAdRemovalErrorModal.react","IGDSecureShareSheetDialog.react","PolarisPostFastOptionsModal.next.react","PolarisPostFastAdDebugToolModal.next.react","PolarisPostFastAdRemovedModal.next.react","PolarisPostFastHideAdModal.next.react","PolarisPostFastReportAdModal.next.react","PolarisPostFastReportModal.next.react","PolarisPostFastShareSheet.next.react","PolarisPostFastTaggedModal.next.react","PolarisPostFastUnfollowModal.next.react","PolarisPostFastWAISTModal.next.react","PolarisPostFastAboutThisAccountModal.next.react","PolarisPostFastCopyModal.next.react","PolarisPostFastDeleteModal.next.react","PolarisPostFastEmbedModal.next.react","PolarisUserHoverCardContentV2.react","PolarisBoostNotDeliveringDialog.react","PolarisPostFastAllowCommentsModal.next.react","PolarisVideoVariantSelector.react","PolarisInformTreatmentSensitivityDetailsDialog.react","PolarisPostFastAdInfoTransparencyModal.next.react","VideoPlayerEmsg"]]],["Bootloader","markComponentsAsImmediate",[],[["CometErrorRoot.react"]]],["RequireDeferredReference","unblock",[],[["PolarisPostRoot.entrypoint","PolarisPostRoot.react"],"sd"]],["RequireDeferredReference","unblock",[],[["PolarisPostRoot.entrypoint","PolarisPostRoot.react"],"css"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierOneBeforeScheduler"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierOneInsideScheduler"]]]}}]]]}</script> +<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierOneEnd"]]]}</script> + +<script type="application/json" data-content-len="105" data-sjs>{"require":[["CometSSRMergedContentInjector","onPayloadReceived",null,[{"status":"fail_ssr_disabled"}]]]}</script> + + <script type="application/json" data-content-len="80" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierTwo"]]]}</script> +<script type="application/json" data-content-len="3654" data-sjs>{"require":[["HasteSupportData","handle",null,[{"clpData":{"5676":{"r":1,"s":1},"5677":{"r":1,"s":1},"1808128":{"r":1,"s":1},"6785":{"r":1,"s":1},"3237":{"r":1,"s":1},"3448":{"r":1},"3450":{"r":1},"4791":{"r":1},"698":{"r":1,"s":1},"5386":{"r":1},"742":{"r":1,"s":1},"744":{"r":1,"s":1},"844":{"r":1},"846":{"r":1},"928":{"r":1,"s":1},"929":{"r":1,"s":1},"934":{"r":1,"s":1}},"gkxData":{"5279":{"result":false,"hash":null},"24333":{"result":false,"hash":null},"1771":{"result":false,"hash":null},"4256":{"result":false,"hash":null},"2036":{"result":false,"hash":null},"4012":{"result":false,"hash":null},"4115":{"result":false,"hash":null},"25373":{"result":false,"hash":null},"773":{"result":true,"hash":null},"1782":{"result":true,"hash":null},"8980":{"result":false,"hash":null},"231":{"result":true,"hash":null},"2268":{"result":true,"hash":null},"2790":{"result":false,"hash":null},"3980":{"result":false,"hash":null},"6117":{"result":false,"hash":null},"6648":{"result":false,"hash":null},"6710":{"result":false,"hash":null},"6792":{"result":false,"hash":null},"6992":{"result":false,"hash":null},"7867":{"result":false,"hash":null},"8483":{"result":false,"hash":null},"8574":{"result":false,"hash":null},"8618":{"result":false,"hash":null},"9160":{"result":true,"hash":null},"9161":{"result":true,"hash":null},"9396":{"result":false,"hash":null},"10410":{"result":false,"hash":null},"10867":{"result":false,"hash":null},"22885":{"result":false,"hash":null},"23390":{"result":false,"hash":null},"23405":{"result":false,"hash":null},"23434":{"result":false,"hash":null},"23913":{"result":false,"hash":null},"24110":{"result":false,"hash":null},"24112":{"result":true,"hash":null},"26360":{"result":true,"hash":null},"26381":{"result":false,"hash":null},"26382":{"result":false,"hash":null},"26383":{"result":false,"hash":null}},"ixData":{"258770":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/oCvaZ5i8BT7.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/oCvaZ5i8BT7.png","w":56,"h":56,"p":"0 0","sz":"auto"},"258773":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/oCvaZ5i8BT7.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/oCvaZ5i8BT7.png","w":56,"h":56,"p":"0 -57px","sz":"auto"},"295614":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/0qSQcHrCNzw.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/0qSQcHrCNzw.png","w":48,"h":48,"p":"0 0","sz":"auto"},"295615":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/0qSQcHrCNzw.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/0qSQcHrCNzw.png","w":48,"h":48,"p":"0 -49px","sz":"auto"},"428522":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ye\/r\/1IbINHIZx7_.gif","width":72,"height":72},"428524":{"sprited":0,"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yI\/r\/Z497IZKp0hc.gif","width":72,"height":72}},"metaconfigData":{"10":{"value":0},"32":{"value":false},"36":{"value":false}},"qexData":{"154":{"r":null},"542":{"r":null},"3401":{"r":null},"170":{"r":null},"1014":{"r":null},"797":{"r":false},"68":{"r":null},"135":{"r":null},"136":{"r":null},"173":{"r":null},"422":{"r":null},"715":{"r":null},"2986":{"r":null},"3207":{"r":null}},"qplData":{"719":{"r":10000},"143":{"r":5},"152":{"r":10},"310":{"r":1},"1127":{"r":12},"1857":{"r":100},"4342":{}},"justknobxData":{"2417":{"r":false},"1495":{"r":false},"1716":{"r":124},"1813":{"r":true},"2003":{"r":false},"2392":{"r":1},"2730":{"r":30000},"2731":{"r":20000},"2800":{"r":30000},"2903":{"r":true},"3895":{"r":true}}}]]]}</script> +<script type="application/json" data-content-len="95583" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1021044255},"rsrcMap":{"oyFxPK0":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iOpM4\/yB\/l\/en_US\/JvMnaVALLiW.js","c":1,"p":":902","m":"1021044255_main"},"+L0X93S":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i6BR4\/y8\/l\/en_US\/_lVFW5f8PmC.js","c":1,"p":":25482","m":"1021044255_longtail"},"OqitpLF":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yw\/r\/24CVUQXA1xW.js","c":1,"p":":25489","m":"1021044255_longtail"},"YfY0wDs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iQvT4\/y7\/l\/en_US\/8b22m6VDHyD.js","c":1,"p":":45","m":"1021044255_main"},"DTsXRT2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yO\/r\/eVx8wfiBcCE.js","c":1,"p":":753","m":"1021044255_main"},"0yrto9t":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iQpj4\/yc\/l\/en_US\/OmHjCOhjNRt.js","c":1,"p":":25488","m":"1021044255_longtail"},"lhm546U":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iufE4\/yc\/l\/en_US\/KUbkPPUzO1d.js","c":1,"p":":1336","m":"1021044255_main"},"2E8n3Wl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iHSL4\/yC\/l\/en_US\/Ob2HnRtVDrh.js","c":1,"p":":475","m":"1021044255_main"},"fW53b8+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iW-u4\/yU\/l\/en_US\/81DGp8qdWP7.js","c":1,"p":":473","m":"1021044255_main"},"UCPDyq8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iUcw4\/yz\/l\/en_US\/dQ6r03RuI3a.js","c":1,"p":":867","m":"1021044255_main"},"wJ8iTPu":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idBq4\/yE\/l\/en_US\/NNgzAnyoRuT.js","c":1,"p":":85","m":"1021044255_main"},"XxF9+Kd":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yL\/r\/Iqg5AgKB53K.js","c":1,"p":":44","m":"1021044255_main"},"FXDnn71":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4io_J4\/y5\/l\/en_US\/rhxb4sagAR3.js","c":1,"p":":94","m":"1021044255_main"},"c0LMFtP":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i7M54\/yR\/l\/en_US\/ffLrtj_wWzi.js","c":1,"p":":91","m":"1021044255_main"},"oOuVv3x":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yu\/r\/6FX6W_V-V5G.js","c":1,"p":":54","m":"1021044255_main"},"LSPTPMz":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/wLehvRuVPnZ.js","c":1,"p":":421","m":"1021044255_main"},"2Lk1vvm":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iuEr4\/yD\/l\/en_US\/AHbfmzpUQyS.js","c":1,"p":":100","m":"1021044255_main"},"6Z\/VzZs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ipoo4\/yo\/l\/en_US\/PkoWOK6VkFT.js","c":1,"p":":1064","m":"1021044255_main"},"SrRwK8Q":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/bLEHF9MK5q8.js","c":1,"p":":22162","m":"1021044255_longtail"},"GVFAnwq":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iFOp4\/yX\/l\/en_US\/-peeRDguBok.js","c":1,"p":":948","m":"1021044255_main"},"G+4EpB3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i9I74\/yL\/l\/en_US\/TtLaxtbcsE0.js","c":1,"p":":25538","m":"1021044255_longtail"},"P5iP1N3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ibsH4\/yP\/l\/en_US\/fGtDiGHbrq3.js","c":1,"p":":25545","m":"1021044255_longtail"},"kYm0ofR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4isHY4\/yA\/l\/en_US\/QLIkNXNQom0.js","c":1,"p":":155","m":"1021044255_main"},"Hcz60e2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iRVs4\/yA\/l\/en_US\/If-zwnP4nQM.js","c":1,"p":":556","m":"1021044255_main"},"7Q36uSj":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yj\/r\/xBCSOiwcteq.js","c":1,"p":":25531","m":"1021044255_longtail"},"oG8n01r":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDxO4\/yh\/l\/en_US\/vDgZFW80Rog.js","c":1,"p":":25752","m":"1021044255_longtail"},"Xqn1\/+S":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ixT_4\/yk\/l\/en_US\/LCephRifkQy.js","c":1,"p":":540","m":"1021044255_main"},"LT3F25+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yn\/r\/097eHIx1tc_.js","c":1,"p":":25563","m":"1021044255_longtail"},"woIFjSG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iD6v4\/yZ\/l\/en_US\/ovH8-n-Gd1i.js","c":1,"p":":300","m":"1021044255_main"},"zrybfqa":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/tlaQkWLxyaX.js","c":1,"p":":25532","m":"1021044255_longtail"},"8O17rO\/":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i8wC4\/yX\/l\/en_US\/6sZUeVTvi9l.js","c":1,"p":":1048","m":"1021044255_main"},"Ak3iQ5z":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4inC_4\/yP\/l\/en_US\/veUbdTbjtSh.js","c":1,"p":":101","m":"1021044255_main"},"WSU2dim":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ym\/r\/jcOH3UU_TUh.js","c":1,"p":":79","m":"1021044255_main"},"27APACz":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i9xi4\/y8\/l\/en_US\/CwaMBDBKL_J.js","c":1,"p":":76","m":"1021044255_main"},"tgXRZ1+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/6mvWhqrjAuJ.js","c":1,"p":":1008","m":"1021044255_main"},"UTmFS3q":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iL834\/yg\/l\/en_US\/9YJcT4fle1M.js","c":1,"p":":220","m":"1021044255_main"},"bjs5pn2":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/y9\/l\/0,cross\/WSgTbCm4Zcz.css","c":1,"p":":424","m":"1021044255_main"},"h1XzsAu":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yZ\/r\/9hVtr5M_H2P.js","c":1,"p":":25559","m":"1021044255_longtail"},"oxtwk3g":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDnF4\/yi\/l\/en_US\/5Svenvs2UD0.js","c":1,"p":":447","m":"1021044255_main"},"Hrxw3iB":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iyTh4\/y4\/l\/en_US\/xAskb_X2h25.js","c":1,"p":":158","m":"1021044255_main"},"\/T36NKK":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDxX4\/yM\/l\/en_US\/7vNhQ9Ad2yp.js","c":1,"p":":649","m":"1021044255_main"},"ureOrVA":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i9WG4\/yN\/l\/en_US\/a08BkFJV86G.js","c":1,"p":":306","m":"1021044255_main"},"GcdGQVo":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yx\/r\/G2Fe5sPCtbx.js","c":1,"p":":953","m":"1021044255_main"},"k6z4O\/n":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yF\/r\/Ey9mqeqcqFG.js","c":1,"p":":25561","m":"1021044255_longtail"},"kEp9z97":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iRVs4\/yC\/l\/en_US\/VLYobLENq8b.js","c":1,"p":":488","m":"1021044255_main"},"5atnOB5":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ibzq4\/y7\/l\/en_US\/huO--W3YpnH.js","c":1,"p":":84","m":"1021044255_main"},"bJ922yg":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/2jr_tFUjDMy.js","c":1,"p":":42","m":"1021044255_main"},"S\/aXvj0":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iN5-4\/yB\/l\/en_US\/fIM4kpgk-fR.js","c":1,"p":":474","m":"1021044255_main"},"8vhNe03":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/xak5SOWMe6q.js","c":1,"p":":1335","m":"1021044255_main"},"yaG0NB3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y3\/r\/VOuIHcnSqr7.js","c":1,"p":":820","m":"1021044255_main"},"I+eAn9m":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/1MgRChitI8i.js","c":1,"p":":72","m":"1021044255_main"},"ZZdEDIY":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i99Q4\/yC\/l\/en_US\/2M7HCG44Wj1.js","c":1,"p":":347","m":"1021044255_main"},"7DFvnpn":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yU\/r\/AIpNjk6q0O3.js","c":1,"p":":799","m":"1021044255_main"},"I7vervl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idBq4\/y0\/l\/en_US\/_IuWjEvRAqs.js","c":1,"p":":324","m":"1021044255_main"},"odjyOlF":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i5Rr4\/ye\/l\/en_US\/P8l1OnGfEro.js","c":1,"p":":1068","m":"1021044255_main"},"+eI0xLl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yg\/r\/hghqVIbaIaD.js","c":1,"p":":1198","m":"1021044255_main"},"3XPbIEt":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/j9kbEWA5Mcs.js","c":1,"p":":25546","m":"1021044255_longtail"},"pB+Pi9e":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/TO4iZPkNVOZ.js","c":1,"p":":1184","m":"1021044255_main"},"qQ\/MDOJ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iSzg4\/yP\/l\/en_US\/kv9OH80ftQO.js","c":1,"p":":968","m":"1021044255_main"},"8waN9Vk":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yk\/r\/Dhi7TKAnagN.js","c":1,"p":":25557","m":"1021044255_longtail"},"zyHop+P":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ipUw4\/y_\/l\/en_US\/FMNnhu8t3xB.js","c":1,"p":":350","m":"1021044255_main"},"\/8UB4wS":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yq\/r\/LSpkLc8o9Ke.js","c":1,"p":":117","m":"1021044255_main"},"OWEHjib":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i4iG4\/yd\/l\/en_US\/Yi7YhqfhXN8.js","c":1,"p":":25556","m":"1021044255_longtail"},"6QA+JfU":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yv\/r\/jkCWRj1rLM-.js","c":1,"p":":25558","m":"1021044255_longtail"},"O9jznEn":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/HSgHopO38Z8.js","c":1,"p":":674","m":"1021044255_main"},"rELW7ud":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i5Rr4\/y1\/l\/en_US\/zRI_RnDvghb.js","c":1,"p":":25562","m":"1021044255_longtail"},"FLoLv+q":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yR\/r\/h5mhP7AHoGb.js","c":1,"p":":835","m":"1021044255_main"},"M3JbCve":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ifx34\/y7\/l\/en_US\/rdY0eU9NF4i.js","c":1,"p":":156","m":"1021044255_main"},"U3i9xDA":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iMZq4\/yp\/l\/en_US\/2iBEDVVHS7C.js","c":1,"p":":116","m":"1021044255_main"},"8zuTsDC":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/OMgQjEFR_Qu.js","c":1,"p":":1242","m":"1021044255_main"},"ANxwiYz":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/clUoAjHZKcP.js","c":1,"p":":1006","m":"1021044255_main"},"e6O+MOU":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y5\/r\/W-5zAraazm5.js","c":1,"p":":25560","m":"1021044255_longtail"},"W74069i":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDxX4\/ym\/l\/en_US\/NY0V0kcpqp5.js","c":1,"p":":956","m":"1021044255_main"},"CsM+zFT":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y8\/r\/lezphXcVpoh.js","c":1,"p":":954","m":"1021044255_main"},"9ur9Im7":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i3014\/ym\/l\/en_US\/KsNqjGBQNaI.js","c":1,"p":":122","m":"1021044255_main"},"KJ+69wK":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ik3t4\/yc\/l\/en_US\/Efo_dGv3r1g.js","c":1,"p":":466","m":"1021044255_main"},"N86a77B":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iAH84\/y8\/l\/en_US\/C-tTu0dWimC.js","c":1,"p":":1187","m":"1021044255_main"},"ki+fF7f":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/El4EjK8qY0H.js","c":1,"p":":111","m":"1021044255_main"},"UQ1s16d":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yu\/l\/0,cross\/xIfzcpbbLId.css","c":1,"p":":427","m":"1021044255_main"},"mue1Fvx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y3\/r\/7pdzqnsn-0_.js","c":1,"p":":549","m":"1021044255_main"},"Z79Apzl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iYPB4\/yC\/l\/en_US\/GfpDQMagS3k.js","c":1,"p":":361","m":"1021044255_main"},"cXBEfvu":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/V-ZARW6OQlR.js","c":1,"p":":544","m":"1021044255_main"},"yEm0muZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iiJn4\/yg\/l\/en_US\/K5i-eZ2WvTN.js","c":1,"p":":105","m":"1021044255_main"},"ItansHP":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ibS84\/yO\/l\/en_US\/dlfx0afOmXE.js","c":1,"p":":25555","m":"1021044255_longtail"},"gPvW2ev":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iK2l4\/y-\/l\/en_US\/YbE7xnfDLeh.js","c":1,"p":":531","m":"1021044255_main"},"PF2V8OI":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i0Ae4\/yH\/l\/en_US\/ZjXuII1tkSo.js","c":1,"p":":1148","m":"1021044255_main"},"ugXWwhC":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iRs54\/y7\/l\/en_US\/qmi-PTDDlw2.js","c":1,"p":":429","m":"1021044255_main"},"iTW6IQv":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ix-c4\/y7\/l\/en_US\/R3wT3H3u8Qx.js","c":1,"p":":217","m":"1021044255_main"},"oxyTRSu":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4if1r4\/ys\/l\/en_US\/oB9RT1qM34r.js","c":1,"p":":175","m":"1021044255_main"},"rWvvTTV":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ixrV4\/yr\/l\/en_US\/mSXh06V2tqu.js","c":1,"p":":25503","m":"1021044255_longtail"},"LKuiaRq":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yc\/r\/eMifRHPYLYK.js","c":1,"p":":201","m":"1021044255_main"},"ctSuKUJ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/WiNhNd22A0r.js","c":1,"p":":25500","m":"1021044255_longtail"},"KCzDqPl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4im-f4\/yF\/l\/en_US\/zTE-K__yibc.js","c":1,"p":":800","m":"1021044255_main"},"ME2FNks":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ih3v4\/yk\/l\/en_US\/MBuB5jbfDid.js","c":1,"p":":104","m":"1021044255_main"},"vn6rzag":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iUVt4\/yK\/l\/en_US\/IoXXXzL_Qhu.js","c":1,"p":":25502","m":"1021044255_longtail"},"tsYMs7p":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDVg4\/yn\/l\/en_US\/wGIauzvwEeh.js","c":1,"p":":486","m":"1021044255_main"},"GIduEtA":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i2OO4\/yc\/l\/en_US\/jFubdx6Dvhw.js","c":1,"p":":483","m":"1021044255_main"},"3kjMbS+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iFjH4\/y3\/l\/en_US\/OkDYkUP1Mys.js","c":1,"p":":848","m":"1021044255_main"},"z+jYabC":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/IgSmUVjuOtK.js","c":1,"p":":626","m":"1021044255_main"},"rev36PJ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yi\/r\/c01B2NdQF7E.js","c":1,"p":":25533","m":"1021044255_longtail"},"QMlHqDY":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iz_l4\/yt\/l\/en_US\/ChVDgJTPRSE.js","c":1,"p":":520","m":"1021044255_main"},"2rs5i6J":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ye\/r\/A_vNzXPRrxL.js","c":1,"p":":496","m":"1021044255_main"},"aJ0ciiB":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4imd04\/yf\/l\/en_US\/nRhPcSvtVuV.js","c":1,"p":":331","m":"1021044255_main"},"rQsUDhX":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ifey4\/yl\/l\/en_US\/yA3fdnp-Rdi.js","c":1,"p":":106","m":"1021044255_main"},"grPDV7F":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y9\/r\/Wu7Pb2y5H9k.js","c":1,"p":":787","m":"1021044255_main"},"SUOZjLs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iyJm4\/y_\/l\/en_US\/SgYPZwOl3kf.js","c":1,"p":":758","m":"1021044255_main"},"qP\/E8tb":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iG8j4\/ym\/l\/en_US\/kwYuz20ONGx.js","c":1,"p":":631","m":"1021044255_main"},"YhiApMW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yN\/r\/-TZ3BMI4lr-.js","c":1,"p":":35949","m":"1021044255_longtail"},"byY1R84":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yc\/r\/1qQtu-sEFXT.js","c":1,"p":":23054","m":"1021044255_longtail"},"xF6Ljct":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iQUU4\/yZ\/l\/en_US\/blup31qLyah.js","c":1,"p":":270","m":"1021044255_main"},"L12oFNZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ye\/r\/fT_hx6zN_74.js","c":1,"p":":343","m":"1021044255_main"},"CEssAWZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y5\/r\/3WIK5xlPKm_.js","c":1,"p":":1101","m":"1021044255_main"},"b43zzPr":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y3\/r\/LDczav0pBCH.js","c":1,"p":":374","m":"1021044255_main"},"bPF63lj":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iBeD4\/yr\/l\/en_US\/gWNPdb0j5Aa.js","c":1,"p":":102","m":"1021044255_main"},"lZjrhqL":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iyeB4\/yR\/l\/en_US\/zwAysWRdQAQ.js","c":1,"p":":131","m":"1021044255_main"},"K+URjhf":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ifXX4\/yJ\/l\/en_US\/Xzq-GpdGh3N.js","c":1,"p":":110","m":"1021044255_main"},"Wktplt+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y2\/r\/5o78UId_2-u.js","c":1,"p":":391","m":"1021044255_main"},"tzbl8BH":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ircy4\/yx\/l\/en_US\/pE4NkC7AiN4.js","c":1,"p":":1090","m":"1021044255_main"},"eGbZ8YT":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yk\/r\/NTgiuJso0nu.js","c":1,"p":":25452","m":"1021044255_longtail"},"0tr8X2v":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iMk-4\/yF\/l\/en_US\/CSW1Og663f9.js","c":1,"p":":25435","m":"1021044255_longtail"},"ALBhwn6":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDyq4\/yM\/l\/en_US\/JmyonZ_Hbr6.js","c":1,"p":":20851","m":"1021044255_longtail"},"s4eF10q":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y_\/r\/XqVhIOQsuBx.js","c":1,"p":":312","m":"1021044255_main"},"RtbN8WO":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ivYD4\/yv\/l\/en_US\/qbMe0cKZyU4.js","c":1,"p":":459","m":"1021044255_main"},"WpvFyQK":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/ym\/l\/0,cross\/zupAm9R8u1G.css","c":1,"p":":172","m":"1021044255_main"},"UQS9J7g":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iP-V4\/yS\/l\/en_US\/LA-z6FGQZjm.js","c":1,"p":":170","m":"1021044255_main"},"KBZBs5O":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iDs44\/ys\/l\/en_US\/J9CFw4HdkLB.js","c":1,"p":":338","m":"1021044255_main"},"wox6GUR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yW\/r\/CgGwv68FWVQ.js","c":1,"p":":259","m":"1021044255_main"},"KZmtGYW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i_xx4\/yG\/l\/en_US\/AWYM42S1Fcm.js","c":1,"p":":455","m":"1021044255_main"},"3fx6UYf":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iate4\/yh\/l\/en_US\/Iwmfbx8YJ2v.js","c":1,"p":":224","m":"1021044255_main"},"9n2X0Kl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iLpP4\/yR\/l\/en_US\/bxI-4UmU-ZS.js","c":1,"p":":49","m":"1021044255_main"},"F8O1kbT":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ifFF4\/yp\/l\/en_US\/g2bG712og7t.js","c":1,"p":":162","m":"1021044255_main"},"CJ7ZV8+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iE754\/yc\/l\/en_US\/J94u0QI52_4.js","c":1,"p":":22757","m":"1021044255_longtail"},"SAA+nXN":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4irxS4\/yd\/l\/en_US\/u9gdiO6Py5I.js","c":1,"p":":196","m":"1021044255_main"},"Na1E3Dy":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i5Un4\/yG\/l\/en_US\/aIkuhzb0D1-.js","c":1,"p":":22756","m":"1021044255_longtail"},"DmSJ7nb":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yD\/r\/BfOBp0gN6Nu.js","c":1,"p":":310","m":"1021044255_main"},"q6aT0Zl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/p1R7hLRG018.js","c":1,"p":":147","m":"1021044255_main"},"DO0DIMu":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iQlx4\/yT\/l\/en_US\/dI9eJHBh1_1.js","c":1,"p":":205","m":"1021044255_main"},"n2L8zN8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yp\/r\/ltDZ4ZgLPpq.js","c":1,"p":":93","m":"1021044255_main"},"KtZLmG+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4igmI4\/yB\/l\/en_US\/EPXWmOzTvYi.js","c":1,"p":":298","m":"1021044255_main"},"HQH\/Hrh":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4igHt4\/yr\/l\/en_US\/dwGdwYYsz18.js","c":1,"p":":594","m":"1021044255_main"},"xMOR47C":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ia5G4\/yh\/l\/en_US\/zVJfvYDcOg_.js","c":1,"p":":537","m":"1021044255_main"},"CaW3ujw":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yb\/r\/F7dJp5lmw_R.js","c":1,"p":":22754","m":"1021044255_longtail"},"sbUT0fG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iW5m4\/yt\/l\/en_US\/q96IVyTqFR_.js","c":1,"p":":194","m":"1021044255_main"},"knSQlgx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y0\/r\/dJdR1fC5Aoe.js","c":1,"p":":27428","m":"1021044255_longtail"},"pLnuU4P":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iTxh4\/yg\/l\/en_US\/oc0bOECvoa1.js","c":1,"p":":563","m":"1021044255_main"},"KQGL0hM":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yh\/r\/CFylN8r9sk2.js","c":1,"p":":210","m":"1021044255_main"},"vK6wenv":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yn\/r\/eGiDBZ0oHoP.js","c":1,"p":":20841","m":"1021044255_longtail"},"MFw7f6W":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yF\/r\/JyvxcC8Xt9y.js","c":1,"p":":130","m":"1021044255_main"},"PMomXZR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i8NS4\/yQ\/l\/en_US\/fcSjqaNfj2G.js","c":1,"p":":346","m":"1021044255_main"},"ttiLvDt":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/zSs8-8D2m-Q.js","c":1,"p":":27423","m":"1021044255_longtail"},"BZbiliY":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iVBC4\/y2\/l\/en_US\/th9S6rIVKJA.js","c":1,"p":":593","m":"1021044255_main"},"Bjrpbc6":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yf\/r\/1vPQq4tBXRM.js","c":1,"p":":108","m":"1021044255_main"},"VUl\/292":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y0\/r\/hNUyjGCpMBD.js","c":1,"p":":882","m":"1021044255_main"},"oTrpkd6":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/ODzk7-I6oH4.js","c":1,"p":":27427","m":"1021044255_longtail"},"T6r6Egc":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ib4W4\/yT\/l\/en_US\/uiGP9jlRfNQ.js","c":1,"p":":871","m":"1021044255_main"},"nokq8md":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yA\/r\/vSyzSHM8C--.js","c":1,"p":":103","m":"1021044255_main"},"v7sPgju":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iOyV4\/yR\/l\/en_US\/kGcScb7Tzd7.js","c":1,"p":":20832","m":"1021044255_longtail"},"W+TDAGO":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/yeOH4X6mS8A.js","c":1,"p":":27425","m":"1021044255_longtail"},"cr2zOh8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4irkz4\/y6\/l\/en_US\/ykz7RhsJg0X.js","c":1,"p":":1179","m":"1021044255_main"},"wmt6DTl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/3ItFgxenUNZ.js","c":1,"p":":20840","m":"1021044255_longtail"},"Qf5nEl3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iv6I4\/y-\/l\/en_US\/lQG4eDLrvKZ.js","c":1,"p":":293","m":"1021044255_main"},"NoAZAO0":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i4Fk4\/yn\/l\/en_US\/EoKYns8BRMm.js","c":1,"p":":872","m":"1021044255_main"},"U\/3Ld\/d":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/BLmVBFfuWGw.js","c":1,"p":":1210","m":"1021044255_main"},"JxxWPhi":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/DQLgJnQqmyp.js","c":1,"p":":301","m":"1021044255_main"},"12euusk":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/8gI_FuPYx5l.js","c":1,"p":":22265","m":"1021044255_longtail"},"r01o1aa":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yO\/r\/CfFeXkSCm61.js","c":1,"p":":35405","m":"1021044255_longtail"},"X2Qra8Y":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/j42zAYfFvoK.js","c":1,"p":":806","m":"1021044255_main"},"fHUdg2x":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y0\/r\/3cnwJOaUjgz.js","c":1,"p":":36163","m":"1021044255_longtail"},"qeBm3fk":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idBq4\/yY\/l\/en_US\/FKTUPJASM-d.js","c":1,"p":":592","m":"1021044255_main"},"\/NTWV2I":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ipLN4\/yn\/l\/en_US\/_S6PVKtnK1c.js","c":1,"p":":20825","m":"1021044255_longtail"},"vRIf6Oc":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yv\/r\/dUqqhG17cwt.js","c":1,"p":":1095","m":"1021044255_main"},"+g4Hsus":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iSaX4\/yL\/l\/en_US\/1XplrfgHI85.js","c":1,"p":":140","m":"1021044255_main"},"GVIxfT8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yN\/r\/RDoyathrPQf.js","c":1,"p":":82","m":"1021044255_main"},"bzBvxqm":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/wYQq9JiHq6z.js","c":1,"p":":880","m":"1021044255_main"},"aGkefgE":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yk\/r\/5HltFR5NT2y.js","c":1,"p":":27044","m":"1021044255_longtail"},"Q3N8ZKv":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iw_C4\/y0\/l\/en_US\/k84F0Iv2BK1.js","c":1,"p":":751","m":"1021044255_main"},"CJusTSZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iwmh4\/y8\/l\/en_US\/bm8Jt-pmaNQ.js","c":1,"p":":27041","m":"1021044255_longtail"},"HBOwAIG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yJ\/r\/-ZQunZ4oxg5.js","c":1,"p":":27047","m":"1021044255_longtail"},"nga4cFF":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yH\/r\/1UsQWi5qLai.js","c":1,"p":":157","m":"1021044255_main"},"9rvGgMZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ijoE4\/yR\/l\/en_US\/dFbB3Pp1EIm.js","c":1,"p":":1293","m":"1021044255_main"},"BJQXboE":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yc\/r\/twjyED5u5gJ.js","c":1,"p":":27043","m":"1021044255_longtail"},"8ghYTK\/":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y5\/r\/3RNCGdSXgIu.js","c":1,"p":":567","m":"1021044255_main"},"G\/RswIW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yC\/r\/yOAwcKvbv3P.js","c":1,"p":":27045","m":"1021044255_longtail"},"U92WFR4":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/YIrXhFeLuse.js","c":1,"p":":552","m":"1021044255_main"},"ThGTKWs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yb\/r\/fEURLD9QYmX.js","c":1,"p":":27046","m":"1021044255_longtail"},"Bv+z1YO":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4igFL4\/yl\/l\/en_US\/1EzArZ9BIN_.js","c":1,"p":":1295","m":"1021044255_main"},"hAW\/IbR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yf\/r\/k-P7rrkJ3-y.js","c":1,"p":":885","m":"1021044255_main"},"ak2ZGzA":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yA\/r\/ICG7WGgV0xE.js","c":1,"p":":1252","m":"1021044255_main"},"cKR+OCs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yP\/r\/LnSAlz_hBbB.js","c":1,"p":":769","m":"1021044255_main"},"B3\/tin7":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yd\/r\/jcx2olK0Rtx.js","c":1,"p":":617","m":"1021044255_main"},"TpfwNJU":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i_ep4\/yz\/l\/en_US\/_YOvfZgENM1.js","c":1,"p":":214","m":"1021044255_main"},"z7Pkncw":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ixt54\/yx\/l\/en_US\/jo3AvMwwK6x.js","c":1,"p":":28524","m":"1021044255_longtail"},"7b+xJ\/a":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i1nX4\/ys\/l\/en_US\/pc__29u21nV.js","c":1,"p":":457","m":"1021044255_main"},"58rEmRC":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ym\/r\/HC2vCMJ6MJu.js","c":1,"p":":841","m":"1021044255_main"},"5\/VlgVx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i4Cv4\/ya\/l\/en_US\/cmE-DDcQsih.js","c":1,"p":":235","m":"1021044255_main"},"C2iVshz":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i-c04\/yr\/l\/en_US\/XJwgM9NKsLL.js","c":1,"p":":97","m":"1021044255_main"},"hxXV5dM":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iw_F4\/yJ\/l\/en_US\/3b4-hhb29rc.js","c":1,"p":":443","m":"1021044255_main"},"blcA9Ra":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yh\/r\/D9UAt8osFee.js","c":1,"p":":165","m":"1021044255_main"},"a6wZSt\/":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ih0G4\/y3\/l\/en_US\/5IX7MDB7KMG.js","c":1,"p":":27293","m":"1021044255_longtail"},"mQQ7ALU":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yh\/l\/0,cross\/JxE519UEuj_.css","c":1,"p":":267","m":"1021044255_main"},"+p8ZtR2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iCMF4\/yN\/l\/en_US\/JcuKOLWM8il.js","c":1,"p":":999","m":"1021044255_main"},"ZG645kc":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/y3\/l\/0,cross\/Lfx2B8Ffg3o.css","c":1,"p":":1000","m":"1021044255_main"},"r\/bGP4G":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yq\/r\/a7D9qBeHtfi.js","c":1,"p":":248","m":"1021044255_main"},"USblxV0":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yH\/r\/XhwRnIHcnxv.js","c":1,"p":":713","m":"1021044255_main"},"u4wikxg":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/y1\/l\/0,cross\/hYj7xnJNJoH.css","c":1,"p":":282","m":"1021044255_main"},"5l4OvG1":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/LjlGg0X2Gn0.js","c":1,"p":":90","m":"1021044255_main"},"JdRWTp+":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yA\/r\/TZ3G418rTva.js","c":1,"p":":96","m":"1021044255_main"},"wvcA3Vp":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/5owFDuKo8Qb.js","c":1,"p":":86","m":"1021044255_main"},"woI9yTK":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i2TF4\/yN\/l\/en_US\/d6i7eaftVeA.js","c":1,"p":":325","m":"1021044255_main"},"YmC\/7Lh":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ik5G4\/yo\/l\/en_US\/qNW40l1FJWp.js","c":1,"p":":247","m":"1021044255_main"},"TM+Et\/5":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ikCk4\/yg\/l\/en_US\/Jea3oO2Ihg9.js","c":1,"p":":297","m":"1021044255_main"},"5Ms0wMJ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yW\/r\/UU_HV03PoFG.js","c":1,"p":":299","m":"1021044255_main"},"ZpOblqZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y4\/r\/bjp6R0-0Yse.js","c":1,"p":":435","m":"1021044255_main"},"+UEIrP3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/Scs270geWHe.js","c":1,"p":":437","m":"1021044255_main"},"uEasYsv":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ia5i4\/yx\/l\/en_US\/w-PdxndOMbJ.js","c":1,"p":":432","m":"1021044255_main"},"s70Y0up":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/b9hDuM6gdCE.js","c":1,"p":":442","m":"1021044255_main"},"yIso0ch":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y2\/r\/LGQAqa8O7pb.js","c":1,"p":":305","m":"1021044255_main"},"xeMNMrj":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y9\/r\/WlXDDW9pyu1.js","c":1,"p":":462","m":"1021044255_main"},"gIZQ3gi":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/SKD8kiILuv_.js","c":1,"p":":472","m":"1021044255_main"},"kEJE72M":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yq\/r\/2ZyiGL_aO5L.js","c":1,"p":":449","m":"1021044255_main"},"bKgIrEo":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yP\/r\/xPszkTRvuns.js","c":1,"p":":1176","m":"1021044255_main"},"xFzArwt":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yP\/r\/-C0B3KT7bNy.js","c":1,"p":":978","m":"1021044255_main"},"G38htn8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4itrY4\/yU\/l\/en_US\/JyrpUSE2Zkb.js","c":1,"p":":852","m":"1021044255_main"},"88mSqvD":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yW\/r\/ha2IGJusRNF.js","c":1,"p":":187","m":"1021044255_main"},"P5C76ga":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yD\/r\/Fybtn6CNgwo.js","c":1,"p":":28568","m":"1021044255_longtail"},"6EPXgdn":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yi\/r\/Lzz_3AUTIO4.js","c":1,"p":":703","m":"1021044255_main"},"tpUUexR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yu\/r\/EKMKXaPgVCL.js","c":1,"p":":900","m":"1021044255_main"},"IKrNQ5r":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yb\/r\/n3i_IxQajN5.js","c":1,"p":":28567","m":"1021044255_longtail"},"6nWyjIp":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ya\/r\/KqhSUyVtyjb.js","c":1,"p":":28569","m":"1021044255_longtail"},"DiSmcAj":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/CZnZxeCtCKs.js","c":1,"p":":996","m":"1021044255_main"},"mpXTvbQ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/B6n49VQDUhx.js","c":1,"p":":28562","m":"1021044255_longtail"},"R5zhLyp":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ya\/r\/Z4bgk6Nmy01.js","c":1,"p":":981","m":"1021044255_main"},"Fk4+LK5":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iYu44\/yq\/l\/en_US\/I3Wrot87DZs.js","c":1,"p":":28555","m":"1021044255_longtail"},"\/+E2pTs":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yN\/r\/-SZLWX7-lwV.js","c":1,"p":":28565","m":"1021044255_longtail"},"AWFML7C":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yH\/r\/gZCnmJLxmg2.js","c":1,"p":":25404","m":"1021044255_longtail"},"LaNsK3o":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yB\/r\/J58sWztsuP5.js","c":1,"p":":28566","m":"1021044255_longtail"},"q78dA45":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ym\/r\/SDix0SJGT72.js","c":1,"p":":22791","m":"1021044255_longtail"},"pMj+oD2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y7\/r\/4yQ5XeQhj0B.js","c":1,"p":":22797","m":"1021044255_longtail"},"RMi5pzQ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yW\/r\/H9x2gzY1amZ.js","c":1,"p":":1082","m":"1021044255_main"},"eRhFSwX":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ijQE4\/y5\/l\/en_US\/LGBy68YJ6BP.js","c":1,"p":":281","m":"1021044255_main"},"t0dWseN":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i1bm4\/yt\/l\/en_US\/SgcKE2xRUBW.js","c":1,"p":":229","m":"1021044255_main"},"Ns9X1iz":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y5\/r\/jQNyvHrxBKu.js","c":1,"p":":22199","m":"1021044255_longtail"},"FRMc3Kb":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4icRH4\/yr\/l\/en_US\/Kq-I4dTsk4J.js","c":1,"p":":668","m":"1021044255_main"},"EGc2gxk":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yD\/r\/0opvwxBONGq.js","c":1,"p":":797","m":"1021044255_main"},"p0o24Pk":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i8Qe4\/yA\/l\/en_US\/fp2YhVEwpUI.js","c":1,"p":":143","m":"1021044255_main"},"DkHD\/\/x":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ijmy4\/yO\/l\/en_US\/U2aopmwJa7Q.js","c":1,"p":":225","m":"1021044255_main"},"kpvZ794":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iO-_4\/yu\/l\/en_US\/FEOrum-2hlZ.js","c":1,"p":":911","m":"1021044255_main"},"ESDmXFx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i1hs4\/y3\/l\/en_US\/6YpuvfCtHQL.js","c":1,"p":":760","m":"1021044255_main"},"waHKobo":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i6KW4\/yu\/l\/en_US\/bQUXbU49b1H.js","c":1,"p":":1075","m":"1021044255_main"},"TRBcXk8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yi\/r\/KfXA6K7AUAz.js","c":1,"p":":498","m":"1021044255_main"},"V3JqY9K":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yx\/r\/-dHDeC5T0fA.js","c":1,"p":":566","m":"1021044255_main"},"xibmePY":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yD\/r\/a3KnqvtQ1F6.js","c":1,"p":":21989","m":"1021044255_longtail"},"r32UL7F":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yW\/r\/RL988_3a8Yh.js","c":1,"p":":28564","m":"1021044255_longtail"},"gaOrHtK":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/ya\/r\/Bty1S6ATsiF.js","c":1,"p":":22794","m":"1021044255_longtail"},"JwfUHps":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4icJe4\/yK\/l\/en_US\/YQMNM_Guyit.js","c":1,"p":":309","m":"1021044255_main"},"ifci8LL":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yE\/r\/Xt9rAD_XKdS.js","c":1,"p":":25403","m":"1021044255_longtail"},"ir9vjVL":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/i9gkjJQKzrx.js","c":1,"p":":22242","m":"1021044255_longtail"},"jl77r19":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iw0M4\/yT\/l\/en_US\/lLtJtISajvt.js","c":1,"p":":477","m":"1021044255_main"},"pwkHnur":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idbG4\/yW\/l\/en_US\/tlTPi71-tev.js","c":1,"p":":468","m":"1021044255_main"},"a6JHKgI":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i6EY4\/yH\/l\/en_US\/VbDSUOH_VL-.js","c":1,"p":":1143","m":"1021044255_main"},"jn\/p0dm":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/LRz3T9BXfVx.js","c":1,"p":":907","m":"1021044255_main"},"2GourbJ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yJ\/r\/eLP-guEPaxl.js","c":1,"p":":582","m":"1021044255_main"},"GUy78z5":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iMFX4\/y7\/l\/en_US\/4ATMQ7IylVw.js","c":1,"p":":28478","m":"1021044255_longtail"},"SJd39Gw":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yN\/r\/B_YWrNNRke6.js","c":1,"p":":28554","m":"1021044255_longtail"},"pFXBCxq":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iOcV4\/yi\/l\/en_US\/OkczjZL13CR.js","c":1,"p":":237","m":"1021044255_main"},"+waDX8D":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yd\/r\/Nx55Q4tPXKo.js","c":1,"p":":828","m":"1021044255_main"},"IdhaZay":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/B-tnwPpwGCY.js","c":1,"p":":22740","m":"1021044255_longtail"},"cOplohA":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/9bYmaR8yQB-.js","c":1,"p":":22165","m":"1021044255_longtail"},"AHvjowD":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yp\/r\/3twIeovwMhe.js","c":1,"p":":22145","m":"1021044255_longtail"},"Ie4LidB":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/sRw5HnEoCKP.js","c":1,"p":":22142","m":"1021044255_longtail"},"SCeJI9H":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yt\/r\/l-LvRS_n_S1.js","c":1,"p":":22140","m":"1021044255_longtail"},"keBlUsV":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yl\/r\/rNyrGfahEx9.js","c":1,"p":":22143","m":"1021044255_longtail"},"BDXTzMg":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iML14\/yc\/l\/en_US\/r2VdhON2N7k.js","c":1,"p":":22136","m":"1021044255_longtail"},"GDrhVlR":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/gAmR6oH3ZcV.js","c":1,"p":":22141","m":"1021044255_longtail"},"\/DfdRx3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/8c20PEuiAXW.js","c":1,"p":":22144","m":"1021044255_longtail"},"vJH20sm":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y7\/r\/vqmQBLveoQX.js","c":1,"p":":22134","m":"1021044255_longtail"},"ZOIwXyF":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yh\/r\/2mU8JmABAat.js","c":1,"p":":22135","m":"1021044255_longtail"},"UrJSNOG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yo\/r\/S6E_GXHHRLG.js","c":1,"p":":22146","m":"1021044255_longtail"},"9oUaCa8":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yb\/r\/pmmISrbQkg6.js","c":1,"p":":22131","m":"1021044255_longtail"},"S9g\/v2X":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y9\/r\/nr8sWih_fOI.js","c":1,"p":":22133","m":"1021044255_longtail"},"w3kXsbM":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/NxHU1RAT854.js","c":1,"p":":22138","m":"1021044255_longtail"},"+yZZvJx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y3\/r\/pfUl5HMNcio.js","c":1,"p":":22137","m":"1021044255_longtail"},"Qi8\/reo":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yJ\/r\/JzhnPAXLFfv.js","c":1,"p":":22132","m":"1021044255_longtail"},"WHhSLLO":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/L2OWNUjRHDo.js","c":1,"p":":23906","m":"1021044255_longtail"},"LuDoC6D":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y2\/r\/wmft2e44N_0.js","c":1,"p":":25131","m":"1021044255_longtail"},"wjNxzQ7":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yT\/l\/0,cross\/a69Rn-fIbNi.css","c":1,"p":":622","m":"1021044255_main"},"uYO3YMG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iUhc4\/yF\/l\/en_US\/Wv3FBoUOQqH.js","c":1,"p":":630","m":"1021044255_main"},"X4\/gUM4":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yI\/r\/Dj_8Tc7oKfk.js","c":1,"p":":161","m":"1021044255_main"},"\/aBxlgD":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y4\/r\/wroRdE8roHo.js","c":1,"p":":629","m":"1021044255_main"},"u7RlxqU":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yI\/r\/EIWqifORFhq.js","c":1,"p":":63","m":"1021044255_main"},"Dit+sq6":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y5\/r\/wM1pNDgMGim.js","c":1,"p":":125","m":"1021044255_main"},"f0fthWY":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yY\/r\/NpkPeDAH1Gn.js","c":1,"p":":135","m":"1021044255_main"},"5v5PRud":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4is0e4\/ya\/l\/en_US\/53Cy76iIH37.js","c":1,"p":":154","m":"1021044255_main"},"ktOgdMo":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yu\/r\/HTfUFYT5YkM.js","c":1,"p":":160","m":"1021044255_main"},"lZvZ7iW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yd\/r\/yH55prFzWVM.js","c":1,"p":":66","m":"1021044255_main"},"DrIk7xK":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i31N4\/y5\/l\/en_US\/CG-Nk6HDfq-.js","c":1,"p":":89","m":"1021044255_main"},"TYdBqlw":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/zoVKZ7a3Y6s.js","c":1,"p":":126","m":"1021044255_main"},"OCeQZr2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yK\/r\/Wp0_mpKJnXL.js","c":1,"p":":69","m":"1021044255_main"},"G1bGvka":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yT\/r\/ES0WDvw_XdM.js","c":1,"p":":129","m":"1021044255_main"},"M7wIzci":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yR\/r\/QhUqM9qq4Af.js","c":1,"p":":148","m":"1021044255_main"},"FWElShw":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yW\/l\/0,cross\/WYj3DBbyC-X.css","c":1,"p":":395","m":"1021044255_main"},"Wl6HkS5":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yS\/r\/2Uc-tp3l0H0.js","c":1,"p":":120","m":"1021044255_main"},"fr0uDIt":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iE484\/yo\/l\/en_US\/ztH3sDb1SIN.js","c":1,"p":":454","m":"1021044255_main"},"867cnOt":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yv\/r\/RuIywenugA8.js","c":1,"p":":208","m":"1021044255_main"},"r\/P7RhI":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yA\/r\/eLjl6f9jjma.js","c":1,"p":":203","m":"1021044255_main"},"FzI5y7g":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yX\/r\/z2ZJa5f-I77.js","c":1,"p":":124","m":"1021044255_main"},"snY8CPE":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iWw64\/y2\/l\/en_US\/eXs_R8FvU9U.js","c":1,"p":":394","m":"1021044255_main"},"rJdOw1j":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4im534\/yS\/l\/en_US\/ZPY0g95kvh4.js","c":1,"p":":177","m":"1021044255_main"},"kYRLqjW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iVg64\/yv\/l\/en_US\/lxzcIzjb_i0.js","c":1,"p":":114","m":"1021044255_main"},"RMnYulX":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/uH_aUxCGhuH.js","c":1,"p":":65","m":"1021044255_main"},"viR7ZL2":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yZ\/r\/v1Zg26LUF-4.js","c":1,"p":":67","m":"1021044255_main"},"6zlJ8ez":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iF2G4\/y5\/l\/en_US\/WVjUzKdJVzV.js","c":1,"p":":776","m":"1021044255_main"},"dUZFa00":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yx\/r\/ftEJzbErtiT.js","c":1,"p":":766","m":"1021044255_main"},"mBIW8ag":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yd\/r\/HRwP2vokt_3.js","c":1,"p":":26362","m":"1021044255_longtail"},"RpfjlNd":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yg\/r\/cwj1uhblZM4.js","c":1,"p":":1294","m":"1021044255_main"},"gMgV2qx":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iWk54\/yb\/l\/en_US\/4XgIA5oD_4J.js","c":1,"p":":207","m":"1021044255_main"},"3t0FqRW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i0bH4\/yb\/l\/en_US\/m4IGWU9-d-z.js","c":1,"p":":238","m":"1021044255_main"},"Sajgg3S":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/uB6YMeMnA0Q.js","c":1,"p":":64","m":"1021044255_main"},"miJXrhb":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y_\/r\/YXVUpTB_Mlb.js","c":1,"p":":74","m":"1021044255_main"},"duO0cRV":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i2qT4\/y-\/l\/en_US\/_OwDn6hc2Dz.js","c":1,"p":":223","m":"1021044255_main"},"Gxp4P3V":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yf\/r\/6gHV2RdOVrQ.js","c":1,"p":":115","m":"1021044255_main"},"PQul9Ez":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yP\/r\/Bc2LCWPojaB.js","c":1,"p":":121","m":"1021044255_main"},"qRv78Su":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/ya\/l\/0,cross\/uVdlRszf3m4.css","c":1,"p":":260","m":"1021044255_main"}},"compMap":{"CometKeyCommandWrapperDialog.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","oyFxPK0","+L0X93S","XGqFsxj","OqitpLF"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"CometModifiedKeyCommandWrapperDialog.react":{"r":["g7l6vJ7","DTsXRT2","0yrto9t","TiYYQjT","fZxjt1a","Q5jgI9M","oyFxPK0","lhm546U","XGqFsxj","OqitpLF"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"IGDSPro2ProDialog.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","2E8n3Wl","fW53b8+","UCPDyq8"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisCoauthorReviewDialog.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","LSPTPMz","XGqFsxj","2Lk1vvm","6Z\/VzZs"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"VideoPlayerHTML5ApiCea608State":{"r":["g7l6vJ7","TiYYQjT","SrRwK8Q"]},"VideoPlayerHTML5ApiWebVttState":{"r":["GVFAnwq","g7l6vJ7"]},"PolarisPostFastGenAITransparencyModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","G+4EpB3","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisPostFastGenAITransparencyModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","P5iP1N3","XGqFsxj","kYm0ofR","Hcz60e2","7Q36uSj"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisBoostMediaIneligibleErrorDialog.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","oG8n01r","Xqn1\/+S","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"IGWebBloksApp":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisPostFastAdDebugToolModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","LT3F25+","woIFjSG","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastGenAITransparencyZeroClickModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","P5iP1N3","XGqFsxj","kYm0ofR","zrybfqa","Hcz60e2"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisCreateCollectionWithPostModal.react":{"r":["8O17rO\/","fZxjt1a","g7l6vJ7","Ak3iQ5z","Q5jgI9M","TiYYQjT","XGqFsxj","2Lk1vvm","WSU2dim"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAboutThisAccountModal.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","woIFjSG","27APACz","tgXRZ1+"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisPostFastCopyModal.react":{"r":["g7l6vJ7","UTmFS3q","bjs5pn2","TiYYQjT","fZxjt1a","Q5jgI9M","h1XzsAu","oxtwk3g","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisPostFastDeleteModal.react":{"r":["g7l6vJ7","Hrxw3iB","Q5jgI9M","TiYYQjT","fZxjt1a","woIFjSG","\/T36NKK","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastEmbedModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","ureOrVA","XGqFsxj","2Lk1vvm"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastHideAdModal.react":{"r":["fZxjt1a","g7l6vJ7","GcdGQVo","Q5jgI9M","TiYYQjT","XGqFsxj","k6z4O\/n"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastOptionsModal.react":{"r":["g7l6vJ7","UTmFS3q","Q5jgI9M","TiYYQjT","kEp9z97","fZxjt1a","woIFjSG","5atnOB5","bJ922yg","S\/aXvj0","XGqFsxj","2Lk1vvm"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","IGCoreModal.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastReportAdModal.react":{"r":["fZxjt1a","g7l6vJ7","GcdGQVo","Q5jgI9M","TiYYQjT","XGqFsxj","8vhNe03"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastReportModal.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","yaG0NB3"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastShareSheet.react":{"r":["g7l6vJ7","UTmFS3q","bjs5pn2","I+eAn9m","Q5jgI9M","TiYYQjT","fZxjt1a","5atnOB5","ZZdEDIY","27APACz","oxtwk3g","7DFvnpn","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","ExternalShareOptionImpressionFalcoEvent","ExternalShareSucceededFalcoEvent","ShareSheetImpressionFalcoEvent","ExternalShareOptionTappedFalcoEvent","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd","I7vervl"]}},"PolarisPostFastTaggedModal.react":{"r":["g7l6vJ7","odjyOlF","Q5jgI9M","TiYYQjT","fZxjt1a","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastUnfollowModal.react":{"r":["g7l6vJ7","+eI0xLl","Q5jgI9M","TiYYQjT","fZxjt1a","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastWAISTModal.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","woIFjSG","XGqFsxj","3XPbIEt"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFavoritesModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","pB+Pi9e","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisBoostMusicErrorDialog.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","qQ\/MDOJ","Xqn1\/+S","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAdRemovedModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","8waN9Vk","woIFjSG","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAdRemovalErrorModal.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","woIFjSG","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisPostFastOptionsModal.next.react":{"r":["g7l6vJ7","UTmFS3q","Q5jgI9M","TiYYQjT","fZxjt1a","kYm0ofR","woIFjSG","5atnOB5","bJ922yg","S\/aXvj0","zyHop+P","\/8UB4wS","Hcz60e2","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","IGCoreModal.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAdDebugToolModal.next.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","woIFjSG","OWEHjib","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAdRemovedModal.next.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","woIFjSG","6QA+JfU","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastHideAdModal.next.react":{"r":["fZxjt1a","g7l6vJ7","GcdGQVo","Q5jgI9M","TiYYQjT","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastReportAdModal.next.react":{"r":["fZxjt1a","g7l6vJ7","GcdGQVo","Q5jgI9M","TiYYQjT","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastReportModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","yaG0NB3"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastShareSheet.next.react":{"r":["g7l6vJ7","O9jznEn","UTmFS3q","bjs5pn2","I+eAn9m","Q5jgI9M","TiYYQjT","fZxjt1a","5atnOB5","ZZdEDIY","27APACz","oxtwk3g","zyHop+P","\/8UB4wS","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","ExternalShareOptionImpressionFalcoEvent","ExternalShareSucceededFalcoEvent","ShareSheetImpressionFalcoEvent","ExternalShareOptionTappedFalcoEvent","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd","I7vervl"]}},"PolarisPostFastTaggedModal.next.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","rELW7ud","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastUnfollowModal.next.react":{"r":["g7l6vJ7","FLoLv+q","Q5jgI9M","TiYYQjT","fZxjt1a","XGqFsxj","M3JbCve","U3i9xDA"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastWAISTModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","8zuTsDC","woIFjSG","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAboutThisAccountModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","woIFjSG","ANxwiYz","27APACz"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"PolarisPostFastCopyModal.next.react":{"r":["g7l6vJ7","bjs5pn2","TiYYQjT","fZxjt1a","Q5jgI9M","oxtwk3g","XGqFsxj","e6O+MOU"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisPostFastDeleteModal.next.react":{"r":["g7l6vJ7","Hrxw3iB","Q5jgI9M","TiYYQjT","fZxjt1a","woIFjSG","W74069i","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastEmbedModal.next.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","CsM+zFT","XGqFsxj","2Lk1vvm"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisUserHoverCardContentV2.react":{"r":["Q5jgI9M","g7l6vJ7","TiYYQjT","fZxjt1a","9ur9Im7","5atnOB5","ureOrVA","27APACz","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisBoostNotDeliveringDialog.react":{"r":["g7l6vJ7","KJ+69wK","Q5jgI9M","TiYYQjT","fZxjt1a","2E8n3Wl","N86a77B","fW53b8+","ki+fF7f","XGqFsxj","UQ1s16d"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","BillingWizardRootUPLogger","BillingWizardCustomerBehaviorLogger","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd","mue1Fvx","Z79Apzl","cXBEfvu","c0LMFtP","oOuVv3x"]}},"PolarisPostFastAllowCommentsModal.next.react":{"r":["fZxjt1a","g7l6vJ7","yEm0muZ","TiYYQjT","Q5jgI9M","ItansHP","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"PolarisVideoVariantSelector.react":{"r":["RBlh3hC","g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","gPvW2ev","PF2V8OI","ugXWwhC","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisInformTreatmentSensitivityDetailsDialog.react":{"r":["iTW6IQv","fZxjt1a","g7l6vJ7","oxyTRSu","TiYYQjT","rWvvTTV","Q5jgI9M","LKuiaRq","ctSuKUJ","KCzDqPl","ME2FNks","LSPTPMz","vn6rzag","tsYMs7p","GIduEtA","XGqFsxj","3kjMbS+","z+jYabC"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]}},"PolarisPostFastAdInfoTransparencyModal.next.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","P5iP1N3","XGqFsxj","kYm0ofR","rev36PJ","Hcz60e2"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]}},"CometErrorRoot.react":{"r":["g7l6vJ7","QMlHqDY","fZxjt1a","TiYYQjT","2rs5i6J","Q5jgI9M","aJ0ciiB","XGqFsxj"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"VideoPlayerEmsg":{"r":["fZxjt1a","g7l6vJ7","rQsUDhX","grPDV7F","Q5jgI9M","SUOZjLs"]},"PolarisEmojiPopover.react":{"r":["Fo5P8JI","Q5jgI9M","RBlh3hC","g7l6vJ7","TiYYQjT","fZxjt1a","qP\/E8tb","XGqFsxj"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]}},"OzVTTSourceBufferImpl":{"r":["fZxjt1a","g7l6vJ7","Pboc6+2","TiYYQjT","YhiApMW","Q5jgI9M","byY1R84"]},"CometTooltip_DEPRECATED.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","xF6Ljct","XGqFsxj"],"rdfds":{"m":["CometTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["CometSuspenseFalcoEvent","ODS","FbtLogging","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"VultureJSSampleRatesLoader":{"r":["L12oFNZ"],"be":1},"PolarisPrivacyFlowLauncher.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","CEssAWZ"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]},"be":1},"PolarisLoggedOutContentWallDialog.react":{"r":["g7l6vJ7","yEm0muZ","Q5jgI9M","TiYYQjT","fZxjt1a","b43zzPr","rQsUDhX","bPF63lj","lZjrhqL","K+URjhf","Wktplt+","XGqFsxj"],"rdfds":{"m":["CometExceptionDialog.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","PolarisFBSDK","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]},"be":1},"IGSentryFeedbackDialog.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","XGqFsxj","tzbl8BH"],"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"BaseTooltip.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","XGqFsxj"],"rds":{"m":["ODS"]},"be":1},"liveQueryFetchWithWWWInitial":{"r":["g7l6vJ7","TiYYQjT","Q5jgI9M","fZxjt1a"],"rds":{"m":["ContextualConfig","BladeRunnerClient","DGWRequestStreamClient","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"CometNewsRegulationDialog.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","XGqFsxj","eGbZ8YT","0tr8X2v"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"SecuredActionBlockDialog.react":{"r":["g7l6vJ7","ALBhwn6","TiYYQjT","fZxjt1a","XGqFsxj","s4eF10q","Q5jgI9M","RtbN8WO","WpvFyQK","UQS9J7g","KBZBs5O","wox6GUR"],"rdfds":{"m":["CDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react"],"r":["YfY0wDs","wJ8iTPu"]},"be":1},"SecuredActionChallengePasswordDialog.react":{"r":["KZmtGYW","FXDnn71","3fx6UYf","fZxjt1a","g7l6vJ7","9n2X0Kl","F8O1kbT","TiYYQjT","CJ7ZV8+","Q5jgI9M","SAA+nXN","Na1E3Dy","DmSJ7nb","xF6Ljct","q6aT0Zl","DO0DIMu","eJ6xznq","n2L8zN8","KtZLmG+","XGqFsxj","HQH\/Hrh","xMOR47C","CaW3ujw","ki+fF7f","sbUT0fG"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"]},"rds":{"m":["ContextualConfig","BladeRunnerClient","FBBrowserPasswordEncryption","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner","CometRelayEF"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"SecuredActionChallengeCDSPasswordDialog.react":{"r":["knSQlgx","pLnuU4P","iTW6IQv","g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","KQGL0hM","vK6wenv","MFw7f6W","XGqFsxj","PMomXZR","ttiLvDt","s4eF10q","RtbN8WO","DmSJ7nb","BZbiliY","WpvFyQK","Bjrpbc6","VUl\/292","oTrpkd6","q6aT0Zl","T6r6Egc","nokq8md","v7sPgju","W+TDAGO","n2L8zN8","cr2zOh8","KtZLmG+","wmt6DTl","tsYMs7p","UQS9J7g","KBZBs5O","Qf5nEl3","NoAZAO0","U\/3Ld\/d","JxxWPhi","12euusk","r01o1aa"],"rdfds":{"m":["CDSTooltipDeferredImpl.react","CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FxReauthenticationFalcoEvent","ContextualConfig","BladeRunnerClient","SecuredActionExteriorFalcoEvent","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CDSToast.react","CometToast.react","MqttLongPollingRunner"],"r":["X2Qra8Y","wJ8iTPu","YfY0wDs","XxF9+Kd","fHUdg2x","qeBm3fk"]},"be":1},"SecuredActionNoChallengeAvailableCDSDialog.react":{"r":["\/NTWV2I","g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","XGqFsxj","RtbN8WO","WpvFyQK","UQS9J7g"],"rdfds":{"m":["CDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"TwoStepVerificationRoot.react":{"r":["KZmtGYW","iTW6IQv","g7l6vJ7","vRIf6Oc","+g4Hsus","TiYYQjT","fZxjt1a","Q5jgI9M","GVIxfT8","XGqFsxj","PMomXZR","bzBvxqm","s4eF10q","aGkefgE","Q3N8ZKv","RtbN8WO","XxF9+Kd","DmSJ7nb","WpvFyQK","CJusTSZ","HBOwAIG","ki+fF7f","nga4cFF","9rvGgMZ","eJ6xznq","KtZLmG+","BJQXboE","HQH\/Hrh","UQS9J7g","8ghYTK\/","xMOR47C","G\/RswIW","U92WFR4","ThGTKWs","Bv+z1YO","hAW\/IbR","wox6GUR"],"rdfds":{"m":["CDSTooltipDeferredImpl.react","CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"]},"rds":{"m":["ContextualConfig","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs"]},"be":1},"PolarisGranularCookieControlModalContent.react":{"r":["g7l6vJ7","TiYYQjT","ak2ZGzA","fZxjt1a","Q5jgI9M","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"XAsyncRequest":{"r":["g7l6vJ7","YfY0wDs","TiYYQjT","Q5jgI9M","XxF9+Kd","c0LMFtP","oOuVv3x","fZxjt1a"],"rds":{"m":["FbtLogging","IntlQtEventFalcoEvent","ODS"]},"be":1},"PolarisActorGatewayDialog.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","cKR+OCs","TiYYQjT","XGqFsxj"],"rdfds":{"m":["CometTooltipDeferredImpl.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","PolarisFBSDK","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]},"be":1},"PolarisNidoEnrollmentDialog.react":{"r":["iTW6IQv","fZxjt1a","g7l6vJ7","KJ+69wK","TiYYQjT","9ur9Im7","Q5jgI9M","bzBvxqm","Z79Apzl","XGqFsxj","UQ1s16d"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"oz-player-polaris":{"r":["g7l6vJ7","B3\/tin7","Q5jgI9M"],"be":1},"PolarisInformTreatmentDialogRoot.react":{"r":["fZxjt1a","g7l6vJ7","TiYYQjT","Q5jgI9M","ctSuKUJ","vn6rzag","XGqFsxj","TpfwNJU","z7Pkncw"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisCommentLikedByListDialogRoot.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","5atnOB5","7b+xJ\/a","58rEmRC","5\/VlgVx","XGqFsxj","C2iVshz"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisEntityQRModal.react":{"r":["hxXV5dM","g7l6vJ7","blcA9Ra","Q5jgI9M","TiYYQjT","fZxjt1a","a6wZSt\/","mQQ7ALU","XGqFsxj","+p8ZtR2","ZG645kc","2Lk1vvm","r\/bGP4G"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisCommentLikedByListContainer.react":{"r":["iTW6IQv","fZxjt1a","g7l6vJ7","USblxV0","Q5jgI9M","TiYYQjT","u4wikxg","7b+xJ\/a","XGqFsxj","C2iVshz"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["PolarisGlobalUIComponents.react","XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisFBSDK","CometToast.react","IgSignalingListener","ZenonCallWindowController","ZenonCallInviteModel","ZenonParentCallsManager","RTWebCallWindowOpener","delegateZenonCallInviteModel","MqttLongPollingRunner","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometTooltipDeferredImpl.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player"],"r":["FXDnn71","5l4OvG1","JdRWTp+","n2L8zN8","YfY0wDs","wJ8iTPu","XxF9+Kd","9n2X0Kl","wvcA3Vp","c0LMFtP","oOuVv3x","Pboc6+2"]},"be":1},"PolarisLikedByListContainer.react":{"r":["iTW6IQv","fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","u4wikxg","7b+xJ\/a","XGqFsxj","C2iVshz"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["PolarisGlobalUIComponents.react","XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisUnfollowDialog.react","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisFBSDK","CometToast.react","IgSignalingListener","ZenonCallWindowController","ZenonCallInviteModel","ZenonParentCallsManager","RTWebCallWindowOpener","delegateZenonCallInviteModel","MqttLongPollingRunner","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometTooltipDeferredImpl.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","oz-player"],"r":["FXDnn71","5l4OvG1","JdRWTp+","n2L8zN8","YfY0wDs","wJ8iTPu","XxF9+Kd","9n2X0Kl","wvcA3Vp","c0LMFtP","oOuVv3x","Pboc6+2"]},"be":1},"PolarisBlockDialogContainer.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisLoggedOutIntentDialog.react":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","fZxjt1a","rQsUDhX","bPF63lj","5atnOB5","woI9yTK","Wktplt+","XGqFsxj","YmC\/7Lh"],"rdfds":{"m":["CometExceptionDialog.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","PolarisFBSDK","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","FXDnn71","c0LMFtP","oOuVv3x"]},"be":1},"CAAFetaSavePasswordInterstitialDialog.react":{"r":["TM+Et\/5","5Ms0wMJ","g7l6vJ7","TiYYQjT","fZxjt1a","ZpOblqZ","KQGL0hM","Q5jgI9M","XGqFsxj","s4eF10q","RtbN8WO","WpvFyQK","n2L8zN8","+UEIrP3","uEasYsv","UQS9J7g","s70Y0up","JxxWPhi"],"rdfds":{"m":["CDSTooltipDeferredImpl.react","CAAFetaSavePasswordInterstitial.react"],"r":["eJ6xznq","yIso0ch","xeMNMrj","q6aT0Zl","gIZQ3gi","kEJE72M"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","XxF9+Kd","c0LMFtP","oOuVv3x"]},"be":1},"PolarisNidoGraduationDialog.react":{"r":["iTW6IQv","fZxjt1a","g7l6vJ7","bKgIrEo","KJ+69wK","TiYYQjT","Q5jgI9M","bzBvxqm","XGqFsxj","xFzArwt","UQ1s16d"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"FXFetaToSDialog.react":{"r":["G38htn8","GVFAnwq","pLnuU4P","g7l6vJ7","88mSqvD","P5C76ga","6EPXgdn","TiYYQjT","fZxjt1a","KQGL0hM","tpUUexR","Q5jgI9M","GVIxfT8","IKrNQ5r","MFw7f6W","XGqFsxj","6nWyjIp","DiSmcAj","RtbN8WO","WpvFyQK","Bjrpbc6","mpXTvbQ","q6aT0Zl","nokq8md","R5zhLyp","Fk4+LK5","\/+E2pTs","AWFML7C","cr2zOh8","KtZLmG+","LaNsK3o","UQS9J7g","8ghYTK\/","U92WFR4","q78dA45","pMj+oD2"],"rdfds":{"m":["CDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"SaharaCometConsentFlowContextualRoot.react":{"r":["KZmtGYW","3fx6UYf","pLnuU4P","iTW6IQv","RBlh3hC","fZxjt1a","g7l6vJ7","9n2X0Kl","F8O1kbT","88mSqvD","RMi5pzQ","eRhFSwX","TiYYQjT","9ur9Im7","KQGL0hM","Q5jgI9M","GVIxfT8","XGqFsxj","t0dWseN","s4eF10q","Ns9X1iz","FRMc3Kb","SAA+nXN","WpvFyQK","xF6Ljct","q6aT0Zl","EGc2gxk","DO0DIMu","ki+fF7f","LSPTPMz","eJ6xznq","n2L8zN8","KtZLmG+","p0o24Pk","DkHD\/\/x","TpfwNJU","kpvZ794","UQS9J7g","ESDmXFx","waHKobo","KBZBs5O","U92WFR4","NoAZAO0","TRBcXk8","JxxWPhi"],"rdfds":{"m":["CDSTooltipDeferredImpl.react","CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisBlockDialog.next.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","V3JqY9K","27APACz","XGqFsxj"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"PolarisFRXReportReviewScreenModalRoot.react":{"r":["fZxjt1a","g7l6vJ7","Q5jgI9M","TiYYQjT","XGqFsxj","xibmePY","TpfwNJU"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"FXFetaPreMigrationDialog.react":{"r":["G38htn8","GVFAnwq","pLnuU4P","g7l6vJ7","88mSqvD","6EPXgdn","TiYYQjT","fZxjt1a","KQGL0hM","Q5jgI9M","GVIxfT8","MFw7f6W","XGqFsxj","PMomXZR","RtbN8WO","WpvFyQK","Bjrpbc6","mpXTvbQ","q6aT0Zl","nokq8md","R5zhLyp","Fk4+LK5","\/+E2pTs","AWFML7C","cr2zOh8","KtZLmG+","r32UL7F","UQS9J7g","8ghYTK\/","U92WFR4","q78dA45","gaOrHtK"],"rdfds":{"m":["CDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"FXFetaAutoMigrationPromptDialog.react":{"r":["g7l6vJ7","88mSqvD","RMi5pzQ","TiYYQjT","fZxjt1a","KQGL0hM","Q5jgI9M","GVIxfT8","XGqFsxj","PMomXZR","s4eF10q","RtbN8WO","BZbiliY","WpvFyQK","VUl\/292","q6aT0Zl","T6r6Egc","R5zhLyp","AWFML7C","n2L8zN8","KtZLmG+","tsYMs7p","JwfUHps","UQS9J7g","ESDmXFx","ifci8LL","NoAZAO0","JxxWPhi","q78dA45","ir9vjVL"],"rdfds":{"m":["CDSTooltipDeferredImpl.react","CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["ContextualConfig","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"BasePortal.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M"],"rds":{"m":["ODS"]},"be":1},"PolarisProfileOtherOptionsEntrypointDialog.react":{"r":["Q5jgI9M","g7l6vJ7","TiYYQjT","fZxjt1a","jl77r19","27APACz","ME2FNks","XGqFsxj","2Lk1vvm","pwkHnur"],"rdfds":{"m":["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","CometToast.react","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","XxF9+Kd"]},"be":1},"KeyBindDialog.react":{"r":["KZmtGYW","a6JHKgI","g7l6vJ7","jn\/p0dm","2GourbJ","TiYYQjT","fZxjt1a","Q5jgI9M","GUy78z5","DmSJ7nb","SJd39Gw","eJ6xznq","KtZLmG+","XGqFsxj","HQH\/Hrh","kpvZ794","xMOR47C","pFXBCxq","+waDX8D","IdhaZay"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"CometIXTFacebookXfacUniversalTriggerRoot.react":{"r":["cOplohA","AHvjowD","g7l6vJ7","Ie4LidB","SCeJI9H","keBlUsV","BDXTzMg","GDrhVlR","TiYYQjT","fZxjt1a","\/DfdRx3","Q5jgI9M","vJH20sm","ZOIwXyF","UrJSNOG","9oUaCa8","lhm546U","eJ6xznq","S9g\/v2X","XGqFsxj","w3kXsbM","+yZZvJx","Qi8\/reo","WHhSLLO","FXDnn71","n2L8zN8","LuDoC6D"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"]},"rds":{"m":["FbtLogging","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"PolarisBugReportModal.react":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","Q5jgI9M","rQsUDhX","wjNxzQ7","XGqFsxj","DkHD\/\/x","uYO3YMG","X4\/gUM4"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","LSDatabaseDump","BladeRunnerClient","MWLSIndexedDBDump","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner","WATagsLogger","PersistedQueue","GetLsDatabase","CometExceptionDialog.react","FDSTooltipDeferredImpl.react"],"r":["wJ8iTPu","YfY0wDs","GVIxfT8","\/aBxlgD","XxF9+Kd","MFw7f6W","u7RlxqU","Dit+sq6","f0fthWY","Bjrpbc6","nokq8md","5v5PRud","ktOgdMo","lZvZ7iW","DrIk7xK","TYdBqlw","OCeQZr2","blcA9Ra","G1bGvka","M7wIzci","eJ6xznq"]},"be":1},"IGDSecureShareSheetDialog.react":{"r":["fZxjt1a","FWElShw","Q5jgI9M","g7l6vJ7","UTmFS3q","F8O1kbT","I+eAn9m","TiYYQjT","9ur9Im7","GVIxfT8","u7RlxqU","Wl6HkS5","fr0uDIt","5atnOB5","ZZdEDIY","27APACz","867cnOt","r\/P7RhI","DO0DIMu","5v5PRud","ki+fF7f","DrIk7xK","zyHop+P","\/8UB4wS","TYdBqlw","XGqFsxj","TpfwNJU","FzI5y7g","snY8CPE","WSU2dim","rJdOw1j","OCeQZr2","pFXBCxq","X4\/gUM4","kYRLqjW","RMnYulX","eJ6xznq"],"rdfds":{"m":["FDSTooltipDeferredImpl.react"]},"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisDirectMQTT","BladeRunnerClient","IGDAlertDialog.react","ExternalShareOptionImpressionFalcoEvent","ExternalShareSucceededFalcoEvent","ExternalShareOptionTappedFalcoEvent","getMAWCanThreadBeCutoverOnDemandV2","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","GetLsDatabase","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","n2L8zN8","XxF9+Kd","I7vervl"]}},"PolarisPostRoot.react":{"r":["g7l6vJ7","fZxjt1a","Q5jgI9M","TiYYQjT","XGqFsxj"],"rdfds":{"m":["CometTooltipDeferredImpl.react","PolarisPostCommentInput.react","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","oz-player","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq","Fo5P8JI","RBlh3hC","SvL53uB","Pboc6+2"]},"rds":{"m":["PolarisGlobalUIComponents.react","XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisDirectMQTT","PolarisUnfollowDialog.react","PolarisPostBoostButton.react","PolarisPostToastImpl.react","PolarisFBSDK","IGCoreModal.react","getSendPostToInstamadilloRecipient","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometToast.react","IgSignalingListener","ZenonCallWindowController","ZenonCallInviteModel","ZenonParentCallsManager","RTWebCallWindowOpener","delegateZenonCallInviteModel","MqttLongPollingRunner","LSDatabaseSingletonLazyWrapper","GetLsDatabase"],"r":["FXDnn71","5l4OvG1","JdRWTp+","n2L8zN8","YfY0wDs","wJ8iTPu","XxF9+Kd","kYRLqjW","viR7ZL2","DrIk7xK","GVIxfT8","u7RlxqU","lZvZ7iW","OCeQZr2","9n2X0Kl","wvcA3Vp","c0LMFtP","oOuVv3x","RMnYulX"]},"be":1},"IGDBlockUserDialog.react":{"r":["fZxjt1a","g7l6vJ7","F8O1kbT","TiYYQjT","Q5jgI9M","GVIxfT8","u7RlxqU","6zlJ8ez","ki+fF7f","DrIk7xK","XGqFsxj","FzI5y7g","OCeQZr2","RMnYulX"],"rds":{"m":["InstagramODSImpl","ContextualConfig","PolarisLogger","IgBlockFalcoEvent","IgUnblockFalcoEvent","BladeRunnerClient","IgProfileActionFalcoEvent","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","GetLsDatabase","MqttLongPollingRunner"],"r":["wJ8iTPu","YfY0wDs","dUZFa00","XxF9+Kd"]},"be":1},"PolarisFRXWebSendSecureReportAction":{"r":["fZxjt1a","g7l6vJ7","TiYYQjT","GVIxfT8","MFw7f6W","u7RlxqU","Q5jgI9M","mBIW8ag","RpfjlNd","5v5PRud","lZvZ7iW","DrIk7xK","OCeQZr2"],"rds":{"m":["GetLsDatabase"]},"be":1},"PolarisDesktopStoriesPage.react":{"r":["g7l6vJ7","viR7ZL2","Q5jgI9M","oxyTRSu","TiYYQjT","gMgV2qx","fZxjt1a","5l4OvG1","kYm0ofR","JdRWTp+","3t0FqRW","ki+fF7f","Sajgg3S","miJXrhb","DrIk7xK","duO0cRV","Gxp4P3V","XGqFsxj","PQul9Ez","GVIxfT8","SvL53uB"],"rdfds":{"m":["maybeIssueIGDThreadPointQuery","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","oz-player","FDSTooltipDeferredImpl.react"],"r":["u7RlxqU","OCeQZr2","eJ6xznq","RMnYulX","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisStoryModals.react","BladeRunnerClient","CometRelayEF","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisDirectMQTT","getSendStoryToInstamadilloRecipient","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometToast.react","PolarisUnfollowDialog.react","PolarisFBSDK","IGCoreModal.react","CometTooltipDeferredImpl.react","MqttLongPollingRunner","LSDatabaseSingletonLazyWrapper","GetLsDatabase"],"r":["YfY0wDs","wJ8iTPu","UTmFS3q","qRv78Su","woIFjSG","27APACz","bJ922yg","2Lk1vvm","C2iVshz","XxF9+Kd","n2L8zN8","kYRLqjW","lZvZ7iW","c0LMFtP","oOuVv3x","FXDnn71"]},"be":1},"PolarisMobileStoriesPage.react":{"r":["g7l6vJ7","bjs5pn2","Q5jgI9M","oxyTRSu","TiYYQjT","gMgV2qx","fZxjt1a","5l4OvG1","kYm0ofR","JdRWTp+","oxtwk3g","3t0FqRW","ki+fF7f","miJXrhb","duO0cRV","Gxp4P3V","XGqFsxj","PQul9Ez","GVIxfT8","SvL53uB","DrIk7xK"],"rdfds":{"m":["maybeIssueIGDThreadPointQuery","CometExceptionDialog.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","oz-player","FDSTooltipDeferredImpl.react"],"r":["u7RlxqU","OCeQZr2","eJ6xznq","RMnYulX","Pboc6+2"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisStoryModals.react","BladeRunnerClient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisDirectMQTT","getSendStoryToInstamadilloRecipient","VideoPlayerWithLiveVideoInterruptedOverlay.react","CometRelayEF","PolarisUnfollowDialog.react","PolarisFBSDK","IGCoreModal.react","CometTooltipDeferredImpl.react","MqttLongPollingRunner","LSDatabaseSingletonLazyWrapper","CometToast.react","GetLsDatabase"],"r":["YfY0wDs","wJ8iTPu","UTmFS3q","qRv78Su","woIFjSG","27APACz","bJ922yg","Sajgg3S","2Lk1vvm","C2iVshz","XxF9+Kd","n2L8zN8","kYRLqjW","lZvZ7iW","c0LMFtP","oOuVv3x","FXDnn71"]},"be":1}}}]]]}</script> +<script type="application/json" data-content-len="2936" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:8544",["CometIXTMainFacebookXfacUniversalTriggerEntryPoint.entrypoint"],{"__rc":["CometIXTMainFacebookXfacUniversalTriggerEntryPoint.entrypoint",null]},-1],["cr:8592",["useIXTFacebookEntryPointDialog"],{"__rc":["useIXTFacebookEntryPointDialog",null]},-1],["cr:710",[],{"__rc":[null,null]},-1],["cr:3014",["MaybeNativePromise"],{"__rc":["MaybeNativePromise",null]},-1],["cr:8374",["CometIXTFacebookXfacUniversalTriggerEntryPoint.entrypoint"],{"__rc":["CometIXTFacebookXfacUniversalTriggerEntryPoint.entrypoint",null]},-1],["cr:7443",["useCometEntryPointDialog"],{"__rc":["useCometEntryPointDialog",null]},-1]],"require":[["CometExceptionDialog.react"],["SecuredActionTriggerWithAccountID.react"],["SecuredActionTriggerWithEncryptedContext.react"],["FDSTooltipDeferredImpl.react"],["CometTooltipDeferredImpl.react"],["PolarisPostCommentInput.react"],["VideoPlayerSpinner.react"],["VideoPlayerCaptionsArea.react"],["PolarisHttp500UnexpectedErrorPage.react"],["TransportSelectingClientSingletonConditional"],["GlobalVideoPortsImpl.react"],["oz-player"],["emptyFunction","thatReturns",["RequireDeferredReference"],[[{"__dr":"CometExceptionDialog.react"},{"__dr":"SecuredActionTriggerWithAccountID.react"},{"__dr":"SecuredActionTriggerWithEncryptedContext.react"},{"__dr":"FDSTooltipDeferredImpl.react"},{"__dr":"CometTooltipDeferredImpl.react"},{"__dr":"PolarisPostCommentInput.react"},{"__dr":"VideoPlayerSpinner.react"},{"__dr":"VideoPlayerCaptionsArea.react"},{"__dr":"PolarisHttp500UnexpectedErrorPage.react"},{"__dr":"TransportSelectingClientSingletonConditional"},{"__dr":"GlobalVideoPortsImpl.react"},{"__dr":"oz-player"}]]],["Bootloader","markComponentsAsImmediate",[],[["PolarisEmojiPopover.react","OzVTTSourceBufferImpl"]]],["RequireDeferredReference","unblock",[],[["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react","CometTooltipDeferredImpl.react","PolarisPostCommentInput.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","PolarisHttp500UnexpectedErrorPage.react","TransportSelectingClientSingletonConditional","GlobalVideoPortsImpl.react","oz-player"],"sd"]],["RequireDeferredReference","unblock",[],[["CometExceptionDialog.react","SecuredActionTriggerWithAccountID.react","SecuredActionTriggerWithEncryptedContext.react","FDSTooltipDeferredImpl.react","CometTooltipDeferredImpl.react","PolarisPostCommentInput.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","PolarisHttp500UnexpectedErrorPage.react","TransportSelectingClientSingletonConditional","GlobalVideoPortsImpl.react","oz-player"],"css"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierTwoBeforeScheduler"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierTwoInsideScheduler"]]]}}]]]}</script> +<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierTwoEnd"]]]}</script> + +<script type="application/json" data-content-len="82" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierThree"]]]}</script> +<script type="application/json" data-content-len="3845" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"13341":{"uri":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yz\/r\/i4hNazpv6_E.png"}},"clpData":{"3043":{"r":1,"s":1},"5748":{"r":1,"s":1},"5749":{"r":1,"s":1},"5134":{"r":1},"2380":{"r":1,"s":1},"6784":{"r":1,"s":1},"1828945":{"r":100,"s":1},"1848815":{"r":10000,"s":1},"819":{"r":1,"s":1},"1744057":{"r":500,"s":1},"1744058":{"r":5000,"s":1},"1744059":{"r":10000,"s":1},"1744060":{"r":1000,"s":1},"1857112":{"r":1},"1871697":{"r":1,"s":1},"1764786":{"r":1,"s":1},"1755537":{"r":1},"5614":{"r":1},"1823926":{"r":1,"s":1},"1744334":{"r":1,"s":1},"1743882":{"r":1},"1743884":{"r":1},"1744337":{"r":1,"s":1},"1744338":{"r":1,"s":1},"1743095":{"r":1,"s":1},"1750245":{"r":1,"s":1}},"gkxData":{"24007":{"result":false,"hash":"AT7D7HYlifflFcPxEEw"},"25358":{"result":false,"hash":"AT4p7a_rbUvSvEQ5UgU"},"371":{"result":true,"hash":null},"3283":{"result":false,"hash":null},"4246":{"result":true,"hash":null},"5985":{"result":true,"hash":null},"10282":{"result":true,"hash":null},"22843":{"result":false,"hash":null},"23910":{"result":false,"hash":null},"24113":{"result":false,"hash":null},"26390":{"result":false,"hash":null},"2947":{"result":false,"hash":null},"3570":{"result":true,"hash":null},"8566":{"result":false,"hash":null},"24028":{"result":false,"hash":null},"20919":{"result":true,"hash":null},"20929":{"result":true,"hash":null},"21119":{"result":false,"hash":null},"21120":{"result":true,"hash":null},"21121":{"result":false,"hash":null},"21122":{"result":false,"hash":null},"21123":{"result":true,"hash":null},"21124":{"result":false,"hash":null},"1221":{"result":false,"hash":null},"25571":{"result":false,"hash":null},"9999":{"result":true,"hash":null},"20915":{"result":false,"hash":null},"20916":{"result":false,"hash":null},"20918":{"result":false,"hash":null},"20920":{"result":false,"hash":null},"1930":{"result":false,"hash":null},"5285":{"result":false,"hash":null},"8702":{"result":false,"hash":null},"23430":{"result":false,"hash":null},"24226":{"result":false,"hash":null},"24227":{"result":false,"hash":null},"24228":{"result":true,"hash":null},"25307":{"result":true,"hash":null},"25763":{"result":false,"hash":null},"26332":{"result":false,"hash":null},"23949":{"result":false,"hash":null},"26374":{"result":false,"hash":null},"4142":{"result":false,"hash":null},"8953":{"result":false,"hash":null},"2274":{"result":false,"hash":null},"3298":{"result":false,"hash":null},"5683":{"result":false,"hash":null},"11152":{"result":false,"hash":null},"25152":{"result":false,"hash":null},"25162":{"result":false,"hash":null},"25209":{"result":false,"hash":null},"25223":{"result":false,"hash":null},"25224":{"result":false,"hash":null},"25225":{"result":true,"hash":null},"25249":{"result":false,"hash":null},"1083":{"result":false,"hash":null},"25214":{"result":true,"hash":null},"21049":{"result":false,"hash":null},"25151":{"result":false,"hash":null}},"ixData":{"485124":{"sprited":2,"spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","_spi":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yr\/r\/86FqrT4qV43.png","w":24,"h":24,"p":"-97px -97px","sz":"auto"}},"metaconfigData":{"15":{"value":false},"99":{"value":false}},"qexData":{"923":{"r":null},"83":{"r":null},"138":{"r":null},"192":{"r":null},"196":{"r":null},"227":{"r":null},"531":{"r":null},"532":{"r":null}},"qplData":{"817":{"r":1},"1488":{"r":1},"737":{"r":10},"1094":{},"1544":{"r":1},"2039":{"r":1},"2452":{},"3408":{},"6172":{},"8823":{}},"justknobxData":{"1410":{"r":true},"3637":{"r":true},"3638":{"r":true},"317":{"r":16},"1023":{"r":true},"2595":{"r":1},"471":{"r":false},"854":{"r":false},"855":{"r":false},"1019":{"r":true},"1020":{"r":true},"1021":{"r":true},"1155":{"r":true},"1853":{"r":false},"1893":{"r":true},"1977":{"r":true},"3404":{"r":false}}}]]]}</script> +<script type="application/json" data-content-len="7563" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1021044255},"rsrcMap":{"2LoZtGg":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iLui4\/yv\/l\/en_US\/v1Zg26LUF-4ffLrtj_wWziyH55prFzWVMRDoyathrPQfltDZ4ZgLPpqqkaf6dE6uJduH_aUxCGhuHbxI-4UmU-ZSWp0_mpKJnXLYXVUpTB_MlbTZ3G418rTva6FX6W_V-V5GIqg5AgKB53KLjlGg0X2Gn0.js","c":1,"p":":67,91,66,82,93,92,65,49,69,74,96,54,44,90","m":"1021044255_main"},"qWj5f34":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yF\/r\/ehgRd48YRrN.js","c":1,"p":":28527","m":"1021044255_longtail"},"9qrg3z9":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yv\/r\/VzxW1DVlNWc.js","c":1,"p":":21825","m":"1021044255_longtail"},"Wh4RNym":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/ZeXTO-Nsh6X.js","c":1,"p":":242","m":"1021044255_main"},"SVyUCgH":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yk\/l\/0,cross\/kOt82x50GUU.css","c":1,"p":":711","m":"1021044255_main"},"VjqZ4Xl":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iTQy4\/yL\/l\/en_US\/bs3f167WAuC.js","c":1,"p":":676","m":"1021044255_main"},"nrKMN6J":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iCot4\/yb\/l\/en_US\/SWOcNxWoTKu.js","c":1,"p":":222","m":"1021044255_main"},"0GqKe0E":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iNKJ4\/yV\/l\/en_US\/qxZayAaZV-D.js","c":1,"p":":1263","m":"1021044255_main"},"vCW0EWu":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yG\/l\/0,cross\/bUEOUCWAiR-.css","c":1,"p":":612","m":"1021044255_main"},"gxRy\/q4":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yV\/l\/0,cross\/I4yy6Micc2W.css","c":1,"p":":219","m":"1021044255_main"},"1PfwOMZ":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iJfX4\/y4\/l\/en_US\/jOkliLTkfJt.js","c":1,"p":":1138","m":"1021044255_main"},"\/52NV1z":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yJ\/r\/A_4KzbNUYJz.js","c":1,"p":":613","m":"1021044255_main"},"vSO3scE":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yG\/l\/0,cross\/Hf4traYwKaj.css","c":1,"p":":1103","m":"1021044255_main"},"pwMKEnp":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idwg4\/yG\/l\/en_US\/vhLFRClwI6j.js","c":1,"p":":286","m":"1021044255_main"},"+RQCqw\/":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iZm44\/yk\/l\/en_US\/H1gyjZTny9l.js","c":1,"p":":868","m":"1021044255_main"},"ifd+VN3":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yF\/l\/0,cross\/bO6jVonyivt.css","c":1,"p":":1246","m":"1021044255_main"},"HYicvYU":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/_gJdK0kDU_y.js","c":1,"p":":1291","m":"1021044255_main"},"TzfhvaI":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yh\/r\/qpQtzcbhdhi.js","c":1,"p":":27189","m":"1021044255_longtail"},"NmBxp8C":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yv\/r\/x-oZ9Rv2XBn.js","c":1,"p":":27199","m":"1021044255_longtail"},"KAkIewO":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y-\/r\/7nURg1eRrUo.js","c":1,"p":":27206","m":"1021044255_longtail"},"7pxsM9X":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i3f24\/yO\/l\/en_US\/zcVCbMK4YNW.js","c":1,"p":":1065","m":"1021044255_main"},"XIDSIdG":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ikox4\/ya\/l\/en_US\/dNSGCLY7Uv4.js","c":1,"p":":579","m":"1021044255_main"},"HNx87C3":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4ia4n4\/yt\/l\/en_US\/eJONHtd4fDH.js","c":1,"p":":467","m":"1021044255_main"},"emcpxw2":{"type":"css","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/yH\/l\/0,cross\/qxyn_sO5NnR.css","c":1,"p":":507","m":"1021044255_main"},"Fhjv++d":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iFU64\/yn\/l\/en_US\/P6jrBpKwGFz.js","c":1,"p":":606","m":"1021044255_main"},"PsFgX+k":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iV494\/yT\/l\/en_US\/coBOjPsygou.js","c":1,"p":":269","m":"1021044255_main"},"KlpU8BW":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yQ\/r\/1jd0juyUx4o.js","c":1,"p":":25504","m":"1021044255_longtail"},"+mpY3In":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yq\/r\/Y3TYZTQOxgX.js","c":1,"p":":25505","m":"1021044255_longtail"},"Uljhl4S":{"type":"js","src":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yU\/r\/4SszQdJc41C.js","c":1,"p":":528","m":"1021044255_main"}},"compMap":{"IGCallUriBuilder":{"r":["g7l6vJ7","TiYYQjT","fZxjt1a","qWj5f34","Wl6HkS5","Q5jgI9M"],"rds":{"m":["ODS"]}},"setE2eeIsMandatedForZenonLoggers":{"r":["9qrg3z9","g7l6vJ7","2LoZtGg","TiYYQjT","Q5jgI9M","wvcA3Vp","YfY0wDs","fZxjt1a"],"rds":{"m":["MqttLongPollingRunner","FbtLogging","IntlQtEventFalcoEvent","ODS"]}},"ZenonGraphQLMWThriftMessageSender":{"r":["g7l6vJ7","2LoZtGg","TiYYQjT","Q5jgI9M","wvcA3Vp","YfY0wDs","fZxjt1a","Wh4RNym"],"rds":{"m":["MqttLongPollingRunner","FbtLogging","IntlQtEventFalcoEvent","ODS"]}},"Dialog":{"r":["g7l6vJ7","TiYYQjT","Q5jgI9M","SVyUCgH","2LoZtGg","VjqZ4Xl","ki+fF7f","fZxjt1a","nrKMN6J"],"rds":{"m":["FbtLogging","IntlQtEventFalcoEvent","ODS"],"r":["YfY0wDs"]},"be":1},"ExceptionDialog":{"r":["g7l6vJ7","88mSqvD","0GqKe0E","TiYYQjT","fZxjt1a","9ur9Im7","Q5jgI9M","vCW0EWu","u7RlxqU","SVyUCgH","gxRy\/q4","LKuiaRq","2LoZtGg","VjqZ4Xl","Bjrpbc6","1PfwOMZ","bJ922yg","ki+fF7f","\/52NV1z","XGqFsxj","vSO3scE","pwMKEnp","UQS9J7g","+RQCqw\/","nrKMN6J","ifd+VN3","+waDX8D","TRBcXk8"],"rds":{"m":["FbtLogging","ODS","IntlQtEventFalcoEvent"],"r":["YfY0wDs"]},"be":1},"ConfirmationDialog":{"r":["g7l6vJ7","Q5jgI9M","TiYYQjT","2LoZtGg","ki+fF7f","fZxjt1a","nrKMN6J","HYicvYU"],"be":1},"MWADeveloperReauthBarrier":{"r":["g7l6vJ7","TiYYQjT","TzfhvaI","NmBxp8C","KAkIewO"],"be":1},"PolarisLiveCreationModal.react":{"r":["Fo5P8JI","fZxjt1a","Q5jgI9M","g7l6vJ7","7pxsM9X","TiYYQjT","XIDSIdG","SvL53uB","XGqFsxj","ki+fF7f","LSPTPMz","2Lk1vvm","HNx87C3","emcpxw2"],"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","PolarisDirectMQTT","BladeRunnerClient","getSendLiveToInstamadilloRecipient","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner","LSDatabaseSingletonLazyWrapper","GetLsDatabase"],"r":["YfY0wDs","wJ8iTPu","2LoZtGg","kYRLqjW","u7RlxqU","DrIk7xK","Fhjv++d"]},"be":1},"PolarisSwitchAccountsModal.react":{"r":["g7l6vJ7","yEm0muZ","Q5jgI9M","TiYYQjT","fZxjt1a","rQsUDhX","bPF63lj","lZjrhqL","K+URjhf","XGqFsxj","PsFgX+k"],"rdfds":{"m":["CometExceptionDialog.react","FDSTooltipDeferredImpl.react"],"r":["eJ6xznq"]},"rds":{"m":["XAsyncRequest","InstagramODSImpl","ContextualConfig","PolarisLogger","BladeRunnerClient","PolarisFBSDK","FbtLogging","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","MqttLongPollingRunner"],"r":["YfY0wDs","wJ8iTPu","2LoZtGg","FXDnn71"]},"be":1},"IGWebCallWindowInitializer":{"r":["g7l6vJ7","2LoZtGg","TiYYQjT","Q5jgI9M","fZxjt1a","KlpU8BW","+mpY3In"],"rds":{"m":["FbtLogging","IntlQtEventFalcoEvent","ODS"],"r":["YfY0wDs"]},"be":1},"ZenonSignalingClientManager":{"r":["FXDnn71","g7l6vJ7","2LoZtGg","TiYYQjT","Uljhl4S","Q5jgI9M","YfY0wDs","fZxjt1a"],"rds":{"m":["FbtLogging","IntlQtEventFalcoEvent","ODS"]},"be":1}},"sotUpgrades":["wJ8iTPu","YfY0wDs","2LoZtGg","FXDnn71","kYRLqjW","DrIk7xK","u7RlxqU","wvcA3Vp"]}]]]}</script> +<script type="application/json" data-content-len="139" data-sjs>{"require":[["DeferredJSResourceScheduler",null,null,[["wJ8iTPu","YfY0wDs","2LoZtGg","FXDnn71","kYRLqjW","DrIk7xK","u7RlxqU","wvcA3Vp"]]]]}</script> +<script type="application/json" data-content-len="18857" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:6943",["EventListenerImplForCacheStorage"],{"__rc":["EventListenerImplForCacheStorage",null]},-1],["cr:3024",[],{"__rc":[null,null]},-1],["cr:2046346",[],{"__rc":[null,null]},-1],["cr:8906",["goURIWWW"],{"__rc":["goURIWWW",null]},-1],["cr:708886",["EventProfilerSham"],{"__rc":["EventProfilerSham",null]},-1],["cr:8828",[],{"__rc":[null,null]},-1],["cr:1094907",[],{"__rc":[null,null]},-1],["cr:1609",[],{"__rc":[null,null]},-1],["cr:3106",[],{"__rc":[null,null]},-1],["cr:4779",[],{"__rc":[null,null]},-1],["cr:4839",[],{"__rc":[null,null]},-1],["cr:7274",[],{"__rc":[null,null]},-1],["cr:8738",[],{"__rc":[null,null]},-1],["cr:9681",["PolarisQPManager.react"],{"__rc":["PolarisQPManager.react",null]},-1],["cr:1042",["XAsyncRequestWWW"],{"__rc":["XAsyncRequestWWW",null]},-1],["cr:4890",[],{"__rc":[null,null]},-1],["cr:1402",["CometNetworkStatusToast"],{"__rc":["CometNetworkStatusToast",null]},-1],["cr:2718",[],{"__rc":[null,null]},-1],["cr:9041",[],{"__rc":[null,null]},-1],["cr:1201738",[],{"__rc":[null,null]},-1],["cr:1332233",[],{"__rc":[null,null]},-1],["cr:1345969",["AccessibilityWebAssistiveTechTypedLoggerLite"],{"__rc":["AccessibilityWebAssistiveTechTypedLoggerLite",null]},-1],["cr:1516609",["BDCometSignalCollectionTrigger"],{"__rc":["BDCometSignalCollectionTrigger",null]},-1],["cr:665",[],{"__rc":[null,null]},-1],["cr:4489",[],{"__rc":[null,null]},-1],["cr:6218",[],{"__rc":[null,null]},-1],["cr:7000",[],{"__rc":[null,null]},-1],["cr:7351",[],{"__rc":[null,null]},-1],["cr:8659",[],{"__rc":[null,null]},-1],["cr:8915",[],{"__rc":[null,null]},-1],["cr:10099",[],{"__rc":[null,null]},-1],["cr:10200",["MWLSSchemaEphemeral"],{"__rc":["MWLSSchemaEphemeral",null]},-1],["cr:10755",[],{"__rc":[null,null]},-1],["MarauderConfig",[],{"app_version":"1.0.0.0 (1021044255)","gk_enabled":false},31],["CurrentEnvironment",[],{"facebookdotcom":true,"messengerdotcom":false,"workplacedotcom":false,"instagramdotcom":true,"workdotmetadotcom":false,"horizondotmetadotcom":false},827],["RTISubscriptionManagerConfig",[],{"config":{},"autobot":{},"assimilator":{},"unsubscribe_release":true,"bladerunner_www_sandbox":null,"is_intern":false},1081],["MqttWebConfig",[],{"fbid":"0","appID":219994525426954,"endpoint":"wss:\/\/edge-chat.facebook.com\/chat","pollingEndpoint":"https:\/\/edge-chat.facebook.com\/mqtt\/pull","subscribedTopics":[],"capabilities":10,"clientCapabilities":3,"chatVisibility":false,"hostNameOverride":""},3790],["RequestStreamE2EClientSamplingConfig",[],{"sampleRate":100000,"methodToSamplingMultiplier":{"RTCSessionMessage":10000,"Presence":0.01,"FBGQLS:VOD_TICKER_SUBSCRIBE":0.01,"FBGQLS:STORIES_TRAY_SUBSCRIBE":100,"Collabri":0.1,"FBGQLS:WORK_AVAILABILITY_STATUS_FANOUT_SUBSCRIBE":0.1,"FBGQLS:GROUP_UNSEEN_ACTIVITY_SUBSCRIBE":0.1,"FBGQLS:GROUP_RESET_UNSEEN_ACTIVITY_SUBSCRIBE":0.1,"FBGQLS:INTERN_CALENDAR_UPDATE_SUBSCRIBE":0.1,"SKY:gizmo_manage":10000,"FBGQLS:HUDDLE_USERS_REQUESTED_TO_SPEAK_COUNT_SUBSCRIBE":1000,"FBGQLS:FEEDBACK_LIKE_SUBSCRIBE":1}},4501],["AdsManagerReadRegions",[],{"excluded_endpoints":["\/am_tabular","\/column_suggestions"]},7950],["AsyncRequestConfig",[],{"retryOnNetworkError":"1","useFetchStreamAjaxPipeTransport":true},328],["SessionNameConfig",[],{"seed":"0xLw"},757],["WebDevicePerfInfoData",[],{"needsFullUpdate":true,"needsPartialUpdate":false,"shouldLogResourcePerf":false},3977],["DGWWebConfig",[],{"appId":"936619743392459","appVersion":"0","dgwVersion":"2","endpoint":"","fbId":"0","authType":""},5508],["WebStorageMonsterLoggingURI",[],{"uri":null},3032],["cr:6665",[],{"__rc":[null,null]},-1],["cr:6693",[],{"__rc":[null,null]},-1],["cr:8233",[],{"__rc":[null,null]},-1],["cr:8624",[],{"__rc":[null,null]},-1],["cr:8879",[],{"__rc":[null,null]},-1],["QPLMsgTypesSitevarConfig",[],{"QPL_MSG_TYPES":["join","server_media_update","client_media_update","ring"]},6908],["RpWebStateMachineLoggingBlocklist",[],{"EVENT_TYPES":["xstate.init","receiveStateSyncNotifyRequest","receiveGenericDataMessageRequest","sendIceCandidateRequest","dataChannelMessageReceived","dataMessageReceived","receivePingResponse","receiveOverlayConfigServerUpdateRequest","receiveClientInfoRequest","sendClientEventRequest","receiveRoomContextUpdateRequest","sendGenericDataMessageRequest","capabilitiesReceived","receiveCapabilitiesRequest","createDataMessageSubscription","removeDataMessageSubscription","clientInfoReceived","connectionRoomUpdateReceived","iceCandidateReady","messageSent"],"STATES":["terminated","terminating"],"MESSAGE_TYPES":["DATA_MESSAGE","NOTIFY","ICE_CANDIDATE","UPDATE","PING"]},6230],["RpWebMqttEnabledAppIds",[],{"APP_IDS":[2220391788200892,772021112871879,1586666294789976,424940172743869,436761779744620,514771569228061,1358015658191005]},6874],["RoboticsPermission",[],{"is_authorized_robot":false},3486],["CLDRDateFormatConfig",[],{"supportedPHPFormatsKeys":{"D":"E","D g:ia":"Ejm","D M d":"MMMEd","D M d, Y":"yMMMEd","D M j":"MMMEd","D M j, y":"yMMMEd","D, M j":"MMMEd","D, M j, Y":"yMMMEd","F d":"MMMMd","F d, Y":"date_long","F j":"MMMMd","F j, Y":"date_long","F j, Y \u0040 g:i A":"dateTime_long_short","F j, Y g:i a":"dateTime_long_short","F j, Y \u0040 g:i:s A":"dateTime_long_medium","F jS":"MMMMd","F jS, g:ia":"dateTime_long_short","F jS, Y":"date_long","F Y":"yMMMM","g A":"j","G:i":"time_short","g:i":"time_short","g:i a":"time_short","g:i A":"time_short","g:i:s A":"time_medium","g:ia":"time_short","g:iA":"time_short","g:ia F jS, Y":"dateTime_long_short","g:iA l, F jS":"dateTime_full_short","g:ia M jS":"dateTime_medium_short","g:ia, F jS":"dateTime_long_short","g:iA, l M jS":"dateTime_full_short","h:i a":"time_short","h:m:s m\/d\/Y":"dateTime_short_short","j":"d","j F Y":"date_long","l F d, Y":"date_full","l, F d, Y":"date_full","l, F j":"date_full","l, F j, Y":"date_full","l, F jS":"date_full","l, F jS, g:ia":"dateTime_full_short","l, M j":"date_full","l, M j, Y":"date_full","l, M j, Y g:ia":"dateTime_full_short","M d":"MMMd","M d, Y":"date_medium","M d, Y g:ia":"dateTime_medium_short","M d, Y ga":"dateTime_medium_short","M j":"MMMd","M j, Y":"date_medium","M j, Y g:i A":"dateTime_medium_short","M j, Y g:ia":"dateTime_medium_short","M jS, g:ia":"dateTime_medium_short","M y":"yMMM","M Y":"yMMM","M. d":"MMMd","M. d, Y":"date_medium","m\/d":"Md","m\/d\/Y g:ia":"dateTime_short_short","m\/d\/y H:i:s":"dateTime_short_short","n":"M","n\/j":"Md","n\/j, g:ia":"dateTime_short_short","n\/j\/y":"date_short","Y":"y"},"isLocaleInConfigerator":true,"CLDRConfigeratorFormats":{"dateFormats":{"full":"EEEE, MMMM d, y","long":"MMMM d, y","medium":"MMM d, y","short":"M\/d\/yy"},"timeFormats":{"full":"h:mm:ss\u202fa zzzz","long":"h:mm:ss\u202fa z","medium":"h:mm:ss\u202fa","short":"h:mm\u202fa"},"dateTimeFormats":{"full":"{1}, {0}","long":"{1}, {0}","medium":"{1}, {0}","short":"{1}, {0}"},"availableFormats":{"Bh":"h B","Bhm":"h:mm B","Bhms":"h:mm:ss B","E":"ccc","EBhm":"E h:mm B","EBhms":"E h:mm:ss B","EHm":"E HH:mm","EHms":"E HH:mm:ss","Ed":"d E","Ehm":"E h:mm\u202fa","Ehm-alt-ascii":"E h:mm a","Ehms":"E h:mm:ss\u202fa","Ehms-alt-ascii":"E h:mm:ss a","Gy":"y G","GyMMM":"MMM y G","GyMMMEd":"E, MMM d, y G","GyMMMd":"MMM d, y G","GyMd":"M\/d\/y G","H":"HH","Hm":"HH:mm","Hms":"HH:mm:ss","Hmsv":"HH:mm:ss v","Hmv":"HH:mm v","M":"L","MEd":"E, M\/d","MMM":"LLL","MMMEd":"E, MMM d","MMMMW-count-one":"'week' W 'of' MMMM","MMMMW-count-other":"'week' W 'of' MMMM","MMMMd":"MMMM d","MMMd":"MMM d","Md":"M\/d","d":"d","h":"h\u202fa","h-alt-ascii":"h a","hm":"h:mm\u202fa","hm-alt-ascii":"h:mm a","hms":"h:mm:ss\u202fa","hms-alt-ascii":"h:mm:ss a","hmsv":"h:mm:ss\u202fa v","hmsv-alt-ascii":"h:mm:ss a v","hmv":"h:mm\u202fa v","hmv-alt-ascii":"h:mm a v","ms":"mm:ss","y":"y","yM":"M\/y","yMEd":"E, M\/d\/y","yMMM":"MMM y","yMMMEd":"E, MMM d, y","yMMMM":"MMMM y","yMMMd":"MMM d, y","yMd":"M\/d\/y","yQQQ":"QQQ y","yQQQQ":"QQQQ y","yw-count-one":"'week' w 'of' Y","yw-count-other":"'week' w 'of' Y"}},"CLDRRegionalConfigeratorFormats":{"dateFormats":{"full":"EEEE, MMMM d, y","long":"MMMM d, y","medium":"MMM d, y","short":"M\/d\/yy"},"timeFormats":{"full":"h:mm:ss\u202fa zzzz","long":"h:mm:ss\u202fa z","medium":"h:mm:ss\u202fa","short":"h:mm\u202fa"},"dateTimeFormats":{"full":"{1}, {0}","long":"{1}, {0}","medium":"{1}, {0}","short":"{1}, {0}"},"availableFormats":{"Bh":"h B","Bhm":"h:mm B","Bhms":"h:mm:ss B","E":"ccc","EBhm":"E h:mm B","EBhms":"E h:mm:ss B","EHm":"E HH:mm","EHms":"E HH:mm:ss","Ed":"d E","Ehm":"E h:mm\u202fa","Ehm-alt-ascii":"E h:mm a","Ehms":"E h:mm:ss\u202fa","Ehms-alt-ascii":"E h:mm:ss a","Gy":"y G","GyMMM":"MMM y G","GyMMMEd":"E, MMM d, y G","GyMMMd":"MMM d, y G","GyMd":"M\/d\/y G","H":"HH","Hm":"HH:mm","Hms":"HH:mm:ss","Hmsv":"HH:mm:ss v","Hmv":"HH:mm v","M":"L","MEd":"E, M\/d","MMM":"LLL","MMMEd":"E, MMM d","MMMMW-count-one":"'week' W 'of' MMMM","MMMMW-count-other":"'week' W 'of' MMMM","MMMMd":"MMMM d","MMMd":"MMM d","Md":"M\/d","d":"d","h":"h\u202fa","h-alt-ascii":"h a","hm":"h:mm\u202fa","hm-alt-ascii":"h:mm a","hms":"h:mm:ss\u202fa","hms-alt-ascii":"h:mm:ss a","hmsv":"h:mm:ss\u202fa v","hmsv-alt-ascii":"h:mm:ss a v","hmv":"h:mm\u202fa v","hmv-alt-ascii":"h:mm a v","ms":"mm:ss","y":"y","yM":"M\/y","yMEd":"E, M\/d\/y","yMMM":"MMM y","yMMMEd":"E, MMM d, y","yMMMM":"MMMM y","yMMMd":"MMM d, y","yMd":"M\/d\/y","yQQQ":"QQQ y","yQQQQ":"QQQQ y","yw-count-one":"'week' w 'of' Y","yw-count-other":"'week' w 'of' Y"}},"CLDRToPHPSymbolConversion":{"G":"","yyyy":"Y","yy":"y","y":"Y","LLLLL":"M","LLLL":"f","LLL":"M","LL":"m","L":"n","MMMMM":"M","MMMM":"F","MMM":"M","MM":"m","M":"n","dd":"d","d":"j","ccccc":"D","cccc":"l","ccc":"D","cc":"D","c":"D","EEEEE":"D","EEEE":"l","EEE":"D","EE":"D","E":"D","aaaaa":"A","aaaa":"A","aaa":"A","aa":"A","a":"A","bbbbb":"A","bbbb":"A","bbb":"A","bb":"A","b":"A","BBBBB":"A","BBBB":"A","BBB":"A","BB":"A","B":"A","HH":"H","H":"G","hh":"h","h":"g","K":"g","mm":"i","m":"i","ss":"s","s":"s","z":"","zz":"","zzz":""},"intlDateSpecialChars":{"cldrDelimiter":"'","singleQuote":"'","singleQuoteHolder":"*"}},3019],["IsInternSite",[],{"is_intern_site":false},4517],["ZenonPlatformRateLimitSitevarConfig",[],{"default":{"bucket_size":10,"refill_rate":5},"buckets":{"__map":[]}},6146],["RpZenonBinaryThriftSignalingSitevarConfig",[],{"supported_message_types_mqtt":["JOIN","SERVER_MEDIA_UPDATE","HANGUP","ICE_CANDIDATE","RING","DISMISS","CONFERENCE_STATE","ADD_PARTICIPANTS","SUBSCRIPTION","CLIENT_MEDIA_UPDATE","DATA_MESSAGE","REMOVE_PARTICIPANTS","PING","UPDATE","NOTIFY","CLIENT_EVENT","UNSUBSCRIBE","APPROVAL","WAKEUP"],"inbound_supported_message_types_mqtt":["JOIN","SERVER_MEDIA_UPDATE","HANGUP","ICE_CANDIDATE","RING","DISMISS","CONFERENCE_STATE","ADD_PARTICIPANTS","SUBSCRIPTION","CLIENT_MEDIA_UPDATE","DATA_MESSAGE","REMOVE_PARTICIPANTS","PING","UPDATE","NOTIFY","CLIENT_EVENT","UNSUBSCRIBE","APPROVAL","WAKEUP"]},7272],["BDSignalCollectionData",[],{"sc":"{\"t\":1659080345,\"c\":[[30000,838801],[30001,838801],[30002,838801],[30003,838801],[30004,838801],[30005,838801],[30006,573585],[30007,838801],[30008,838801],[30012,838801],[30013,838801],[30015,806033],[30018,806033],[30021,540823],[30022,540817],[30040,806033],[30093,806033],[30094,806033],[30095,806033],[30101,541591],[30102,541591],[30103,541591],[30104,541591],[30106,806039],[30107,806039],[38000,541427],[38001,806643]]}","fds":60,"fda":60,"i":60,"sbs":1,"dbs":100,"bbs":100,"hbi":60,"rt":262144,"hbcbc":2,"hbvbc":0,"hbbi":30,"sid":-1,"hbv":"388406289229082869"},5239],["cr:1012418",["CometRelay"],{"__rc":["CometRelay",null]},-1]],"require":[["ContextualConfig"],["BladeRunnerClient"],["CometToast.react"],["WebDevicePerfInfoLogging"],["CometPixelRatioUpdater"],["CometChromeDome"],["CometBrowserDimensionsLogger"],["FbtLogging"],["ClientConsistencyFalcoEvent"],["DGWRequestStreamClient"],["CometSuspenseFalcoEvent"],["ODS"],["IntlQtEventFalcoEvent"],["PolarisGlobalUIComponents.react"],["XAsyncRequest"],["InstagramODSImpl"],["PolarisLogger"],["CometRelayEF"],["FalcoLoggerTransports"],["PolarisDirectMQTT"],["PolarisUnfollowDialog.react"],["PolarisPostBoostButton.react"],["PolarisPostToastImpl.react"],["PolarisFBSDK"],["IGCoreModal.react"],["getSendPostToInstamadilloRecipient"],["VideoPlayerWithLiveVideoInterruptedOverlay.react"],["PolarisLogPushNotification"],["PolarisPerformanceLogger"],["IGDSPrivateToaster.react"],["PolarisBackgroundFeedDataFetchEager"],["polarisDebugLogODS"],["PolarisInitWindowsPWAKeyCommand"],["CometOnBeforeUnloadDialog.react"],["CometRootDeferredShared"],["emptyFunction","thatReturns",["RequireDeferredReference"],[[{"__dr":"ContextualConfig"},{"__dr":"BladeRunnerClient"},{"__dr":"CometToast.react"},{"__dr":"WebDevicePerfInfoLogging"},{"__dr":"CometPixelRatioUpdater"},{"__dr":"CometChromeDome"},{"__dr":"CometBrowserDimensionsLogger"},{"__dr":"FbtLogging"},{"__dr":"ClientConsistencyFalcoEvent"},{"__dr":"DGWRequestStreamClient"},{"__dr":"CometSuspenseFalcoEvent"},{"__dr":"ODS"},{"__dr":"IntlQtEventFalcoEvent"},{"__dr":"PolarisGlobalUIComponents.react"},{"__dr":"XAsyncRequest"},{"__dr":"InstagramODSImpl"},{"__dr":"PolarisLogger"},{"__dr":"CometRelayEF"},{"__dr":"FalcoLoggerTransports"},{"__dr":"PolarisDirectMQTT"},{"__dr":"PolarisUnfollowDialog.react"},{"__dr":"PolarisPostBoostButton.react"},{"__dr":"PolarisPostToastImpl.react"},{"__dr":"PolarisFBSDK"},{"__dr":"IGCoreModal.react"},{"__dr":"getSendPostToInstamadilloRecipient"},{"__dr":"VideoPlayerWithLiveVideoInterruptedOverlay.react"},{"__dr":"PolarisLogPushNotification"},{"__dr":"PolarisPerformanceLogger"},{"__dr":"IGDSPrivateToaster.react"},{"__dr":"PolarisBackgroundFeedDataFetchEager"},{"__dr":"polarisDebugLogODS"},{"__dr":"PolarisInitWindowsPWAKeyCommand"},{"__dr":"CometOnBeforeUnloadDialog.react"},{"__dr":"CometRootDeferredShared"},{"__dr":"ExternalShareOptionImpressionFalcoEvent"},{"__dr":"ExternalShareSucceededFalcoEvent"},{"__dr":"ShareSheetImpressionFalcoEvent"},{"__dr":"ExternalShareOptionTappedFalcoEvent"},{"__dr":"IGDAlertDialog.react"},{"__dr":"getMAWCanThreadBeCutoverOnDemandV2"},{"__dr":"BillingWizardRootUPLogger"},{"__dr":"BillingWizardCustomerBehaviorLogger"},{"__dr":"GetLsDatabase"}]]],["CometPlatformRootClient","setInitDeferredPayload",[],[{"sketchInfo":null,"userID":0,"deferredCookies":{"_js_ig_did":{"value":"CD5B157A-74AB-4D01-BA90-0939AF84E3DB","expiration_for_js":31536000000,"expiration_for_http":1773938983,"path":"\/","domain":".instagram.com","secure":true,"http_only":true,"first_party_only":true,"add_js_prefix":true,"same_site":"None"},"_js_datr":{"value":"p_XaZ-nzpvd4wGHVkfffm7Or","expiration_for_js":34560000000,"expiration_for_http":1776962983,"path":"\/","domain":".instagram.com","secure":true,"http_only":true,"first_party_only":true,"add_js_prefix":true,"same_site":"None"},"mid":{"value":"Z9r1pwAEAAHnoBnAsKqFNV5SgWX7","expiration_for_js":34560000000,"expiration_for_http":1776962983,"path":"\/","domain":".instagram.com","secure":true,"http_only":true,"first_party_only":false,"add_js_prefix":false,"same_site":"None"}},"blLoggingCavalryFields":{"bl_sample_rate":0,"hr_sample_rate":0,"parent_lid":"7483563829649516416"}}]],["MqttLongPollingRunner"],["Chromedome"],["IgSignalingListener"],["ZenonCallWindowController"],["ZenonCallInviteModel"],["ZenonParentCallsManager"],["RTWebCallWindowOpener"],["delegateZenonCallInviteModel"],["LSDatabaseSingletonLazyWrapper"],["GetLsDatabase"],["Bootloader","markComponentsAsImmediate",[],[["IGCallUriBuilder","IGDSPro2ProDialog.react","PolarisBoostMediaIneligibleErrorDialog.react","PolarisBoostMusicErrorDialog.react","PolarisBoostNotDeliveringDialog.react","setE2eeIsMandatedForZenonLoggers","ZenonGraphQLMWThriftMessageSender","VideoPlayerHTML5ApiCea608State","VideoPlayerHTML5ApiWebVttState"]]],["RequireDeferredReference","unblock",[],[["ContextualConfig","BladeRunnerClient","CometToast.react","WebDevicePerfInfoLogging","CometPixelRatioUpdater","CometChromeDome","CometBrowserDimensionsLogger","FbtLogging","ClientConsistencyFalcoEvent","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisGlobalUIComponents.react","XAsyncRequest","InstagramODSImpl","PolarisLogger","CometRelayEF","FalcoLoggerTransports","PolarisDirectMQTT","PolarisUnfollowDialog.react","PolarisPostBoostButton.react","PolarisPostToastImpl.react","PolarisFBSDK","IGCoreModal.react","getSendPostToInstamadilloRecipient","VideoPlayerWithLiveVideoInterruptedOverlay.react","PolarisLogPushNotification","PolarisPerformanceLogger","IGDSPrivateToaster.react","PolarisBackgroundFeedDataFetchEager","polarisDebugLogODS","PolarisInitWindowsPWAKeyCommand","CometOnBeforeUnloadDialog.react","CometRootDeferredShared","ExternalShareOptionImpressionFalcoEvent","ExternalShareSucceededFalcoEvent","ShareSheetImpressionFalcoEvent","ExternalShareOptionTappedFalcoEvent","IGDAlertDialog.react","getMAWCanThreadBeCutoverOnDemandV2","BillingWizardRootUPLogger","BillingWizardCustomerBehaviorLogger","GetLsDatabase","MqttLongPollingRunner","Chromedome","IgSignalingListener","ZenonCallWindowController","ZenonCallInviteModel","ZenonParentCallsManager","RTWebCallWindowOpener","delegateZenonCallInviteModel","CometExceptionDialog.react","TransportSelectingClientSingletonConditional","LSDatabaseSingletonLazyWrapper","CometTooltipDeferredImpl.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react"],"sd"]],["RequireDeferredReference","unblock",[],[["ContextualConfig","BladeRunnerClient","CometToast.react","WebDevicePerfInfoLogging","CometPixelRatioUpdater","CometChromeDome","CometBrowserDimensionsLogger","FbtLogging","ClientConsistencyFalcoEvent","DGWRequestStreamClient","CometSuspenseFalcoEvent","ODS","IntlQtEventFalcoEvent","PolarisGlobalUIComponents.react","XAsyncRequest","InstagramODSImpl","PolarisLogger","CometRelayEF","FalcoLoggerTransports","PolarisDirectMQTT","PolarisUnfollowDialog.react","PolarisPostBoostButton.react","PolarisPostToastImpl.react","PolarisFBSDK","IGCoreModal.react","getSendPostToInstamadilloRecipient","VideoPlayerWithLiveVideoInterruptedOverlay.react","PolarisLogPushNotification","PolarisPerformanceLogger","IGDSPrivateToaster.react","PolarisBackgroundFeedDataFetchEager","polarisDebugLogODS","PolarisInitWindowsPWAKeyCommand","CometOnBeforeUnloadDialog.react","CometRootDeferredShared","MqttLongPollingRunner","Chromedome","IgSignalingListener","ZenonCallWindowController","ZenonCallInviteModel","ZenonParentCallsManager","RTWebCallWindowOpener","delegateZenonCallInviteModel","CometExceptionDialog.react","TransportSelectingClientSingletonConditional","LSDatabaseSingletonLazyWrapper","CometTooltipDeferredImpl.react","VideoPlayerSpinner.react","VideoPlayerCaptionsArea.react","GetLsDatabase"],"css"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierThreeBeforeScheduler"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierThreeInsideScheduler"]]]}}]]]}</script> +<script type="application/json" data-content-len="85" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416","tierThreeEnd"]]]}</script> + +<script type="application/json" data-content-len="5557" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadMap",null,[{"7483563829649516416":{"js\/55ekwqze7poo8c4k.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yO\/r\/Hk7xfAQ7REy.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"css\/doyvrzvtajw4g04s.pkg,css\/2uoe9zbawfmskwkk.pkg,css\/20cb3qa4xwbo444s.pkg,css\/3aho6engjg6cs8sk.pkg,css\/4vmu0ixgvp2csswk.pkg.__composite__.css":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v5\/ya\/l\/0,cross\/z8FXlCP5lIcKlxREwiTz-jcyf8QhNEWE-MbVu6J6TSgCAgbkPiBd1Gm.css","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/zb12dl4u99wcc8s4.pkg,js\/cvizvgf49b4kksok.pkg,js\/6fzipc2witgkc80k.pkg,js\/7a1ak4fwq3gg0ksk.pkg,js\/51spfloojzoc80k4.pkg,js\/75ab4h6ox4kcc0cw.pkg,js\/2lwgex0hq5usgows.pkg,js\/94k9vdy8qx444c88.pkg,js\/2pn8rthukxusw4s0.pkg,js\/f0tgmc06qa04okk0.pkg,js\/9f32ue9bj0w84o4g.pkg,js\/4bi9q414ncow0s04.pkg,js\/g6hc5ldhutc0cok8.pkg,js\/f14bhdocq7cosswo.pkg,js\/8e6k319nmk4cog8s.pkg,js\/djuwmggfohkwwk00.pkg,js\/4qgadw68qjggkosg.pkg,js\/52fh0n1bceko8kkk.pkg,js\/ahrtiilp48w0oo44.pkg,js\/20pequ3y77c0kcs8.pkg,js\/czqfrbd8wyogocks.pkg,js\/2vjliocydxc00448.pkg,js\/c8wjwqe0ok08ookc.pkg,js\/5b0jr964w44ckkwc.pkg,js\/dwswjg3yamg4wk0g.pkg,js\/75mo9j8pdy4ggg84.pkg,js\/8h0bsq4pb2wwsgoo.pkg,js\/4gweurqm6nwgskw8.pkg.__composite__.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4isWa4\/yw\/l\/en_US\/J5fGoS7CC80ph3lUk6VcUgI4YqdGV654wwnlKoY-CLrK92SZKLkG6-T-XrNAB1iOQtH5VMvuT83qDj1zS-v6ulv3DVZB1D_iy7Ld14PdmJWIgbxNEkM44K5ZUy9JBQP-gOz5izZwocgVl4ekeFrxv_lHobfd6yXjpTEWhHgqsu2IJGUbXmFifo4mwNXUSQN56HJIIGeUNasVCZ6IoIVGmxwaYlCzAHRsIDVc0M5Q1XPQFFb0nNwk_6eLo_DkiE3nhujbeZVC8BMrsD8q__uh0ek0AamIcMjUzA4I_q7KSGwGOomjocpx.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/34kpy3jjb6gwssgg.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iSXb4\/ye\/l\/en_US\/-iUnpybQ1ig.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/c519tvbewco44w4w.pkg,js\/1f00fhwk8wg0w4ss.pkg,js\/43kmpywa0okkw8o8.pkg,js\/5iblutfzjf48wcs0.pkg,js\/51rvz4latns48880.pkg,js\/72fo5wj0bzgo0cwk.pkg,js\/7exe31u3k10kkcwc.pkg,js\/5kvr6qljid4w4soc.pkg,js\/6d4atmedw0g8gswo.pkg,js\/1sfg00lcn72880gc.pkg,js\/3detpniew5444cc4.pkg,js\/1w2vzhemik4gcsow.pkg,js\/3udk2ymjbuqsccow.pkg,js\/c58o3thq3bsc8so4.pkg,js\/6uhfjj8xsjok80ww.pkg,js\/anmzbdd9big4g8sw.pkg,js\/4q3u6llxa10kso4o.pkg,js\/98nst7whbscocgk4.pkg,js\/3cw2ti8n7fs4sc0o.pkg,js\/da5yo8sijl4o8k40.pkg,js\/36amznfosy80wg8o.pkg,js\/csa9qir353c4goc0.pkg,js\/bahkgj3r7s8o4gws.pkg,js\/5evjehbmh0cg44ks.pkg,js\/c4a5gf6fboookkw8.pkg,js\/e9v21qsd5lw0wo04.pkg,js\/ca6e6t4vqo8488k4.pkg,js\/b7a7y0ant8g0gokc.pkg,js\/af613hpbslk4ogss.pkg,js\/b7r8auxl4bkg8osc.pkg.__composite__.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i0MW4\/y-\/l\/en_US\/fyICEbODIMfh2tKnfwPYIhar4GoveLLUOA2A-x-cNjqvAz3lOgBlx2YcMH8nryRnyhLD1A0xKeUcJbZJGGdT2VN_kNrxUTVcs-o4Hw0ibRssiJqKRHNAt_rkF9mGFSb7X5HlHA9uOBNpIBFNjSuT34FTMN2lkHmdOGcu-jHRttwD3Uv_0jtI38B1Z-jjl5PRCK7W269Fr88l1ToDyBCyaJILCwuCY-bZWtS_SzTvsWqehdEtWw397qFyFcaSD9qMEkYMFZyz5yzIrtSg1gl0NX2Tg92UVk1-RfokkNf3cMKSPKuAkuWttwbwf9BVd3qCpLlkdGCSmZ.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/dhg1kb7k0ao0wo0o.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iSC-4\/yX\/l\/en_US\/FBIYf3YthOA.js","refs":["htmlStart","tierTwo"]},"js\/a3jbo0apd2os0gs8.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4is0M4\/yF\/l\/en_US\/XZJhwX0BQq_.js","refs":["htmlStart","tierTwo"]},"js\/4vvzxq2qx2qsw0s4.pkg,js\/br0pyqy0oug40c00.pkg.__composite__.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/y6\/r\/jUzyieFzD7Hg5iOb8HwiL8.js","refs":["htmlStart","tierTwo"]},"js\/auuxw4pgg9c8g4gk.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yG\/r\/5sx9Xd9ch3O.js","refs":["htmlStart","tierTwo"]},"js\/f4bfxk7adkow0gwk.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yw\/r\/61_rxkerdqj.js","refs":["htmlStart","tierTwo"]},"js\/84k404762fwgg80o.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4idBq4\/yE\/l\/en_US\/NNgzAnyoRuT.js","refs":["tierThree"]},"js\/e38o6yqbq6o8gkwc.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iQvT4\/y7\/l\/en_US\/8b22m6VDHyD.js","refs":["tierThree"]},"js\/39idp2nx0jswc4o4.pkg,js\/53qtmqv772g4gwk8.pkg,js\/9ra52m8cgyccwkgw.pkg,js\/bbhqrhbo11c0k8g4.pkg,js\/ctwi706w5igockc4.pkg,js\/48p918o1sksgcw08.pkg,js\/3vjpve11zoaoo0ow.pkg,js\/9lrp09rval0c80k4.pkg,js\/ephtnhs1rns444c0.pkg,js\/aa4ocl5xd0gg0s0c.pkg,js\/brfifxhi39k4k4gk.pkg,js\/1yhx4na6pou808ss.pkg,js\/eparczlcbfcc4w4k.pkg,js\/d4tfthgeo3k0w8sk.pkg.__composite__.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iLui4\/yv\/l\/en_US\/v1Zg26LUF-4ffLrtj_wWziyH55prFzWVMRDoyathrPQfltDZ4ZgLPpqqkaf6dE6uJduH_aUxCGhuHbxI-4UmU-ZSWp0_mpKJnXLYXVUpTB_MlbTZ3G418rTva6FX6W_V-V5GIqg5AgKB53KLjlGg0X2Gn0.js","refs":["tierThree"]},"js\/3bhfby1bpuckoocw.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4io_J4\/y5\/l\/en_US\/rhxb4sagAR3.js","refs":["tierThree"]},"js\/14vtz0ny2dxc08gg.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4iVg64\/yv\/l\/en_US\/lxzcIzjb_i0.js","refs":["tierThree"]},"js\/3zf6k4le6j6sso48.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4i31N4\/y5\/l\/en_US\/CG-Nk6HDfq-.js","refs":["tierThree"]},"js\/acm8nqc3d4gso4s0.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yI\/r\/EIWqifORFhq.js","refs":["tierThree"]},"js\/2256ajauzx6sok44.pkg.js":{"url":"https:\/\/static.cdninstagram.com\/rsrc.php\/v4\/yV\/r\/5owFDuKo8Qb.js","refs":["tierThree"]}}}]]]}</script> +<script type="application/json" data-content-len="69" data-sjs>{"require":[["qplTagServerJS",null,null,[["lastServerTagFlushed"]]]]}</script> +<script type="application/json" data-content-len="2508" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7483563829649516416-server","earlyFlushStart",50]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genPreloadersStart",50]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genPreloadersEnd",50]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genRouteStart",50]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genRouteEnd",54]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genEFRsrcStart",58]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genEFRsrcEnd",64]],["qplTimingsServerJS",null,null,["7483563829649516416-server","htmlStart",73]],["qplTimingsServerJS",null,null,["7483563829649516416-server","earlyFlushEnd",73]],["qplTimingsServerJS",null,null,["7483563829649516416-server","cometPageRender",74]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierOneStart",76]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genRenderSplashScreenStart",76]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genRenderSplashScreenEnd",77]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierOnePreloadersStart",147]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierOnePreloadersEnd",147]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierOneEnd",156]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierOne",162]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierTwoStart",162]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierTwoPreloadersStart",186]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierTwoEnd",243]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierTwo",245]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierTwoPreloadersEnd",245]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierThreeStart",245]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierThreePreloadersStart",259]],["qplTimingsServerJS",null,null,["7483563829649516416-server","genTierThreeEnd",273]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierThree",274]],["qplTimingsServerJS",null,null,["7483563829649516416-server","tierThreePreloadersEnd",274]],["qplTimingsServerJS",null,null,["7483563829649516416-server","responseEnd",274]],["qplTimingsServerJS",null,null,["7483563829649516416-server","responseStart",1]]]}</script> +<script type="application/json" data-content-len="2" data-sjs>{}</script> +<script type="application/json" data-content-len="200" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadBytesSent",null,[{"htmlStart":[112145,112145],"tierOne":[300217,300217],"ssr_error":[181,1024],"tierTwo":[102714,102714],"tierThree":[31024,31024]}]]]}</script> +<div id='has-finished-comet-page' hidden style='display: none;'></div></body></html> +diff --git a/test/fixtures/tesla_mock/deepl-languages-list.json b/test/fixtures/tesla_mock/deepl-languages-list.json@@ -0,0 +1 @@ +[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}] +\ No newline at end of filediff --git a/test/fixtures/tesla_mock/deepl-translation.json b/test/fixtures/tesla_mock/deepl-translation.json@@ -0,0 +1 @@ +{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]} +\ No newline at end of filediff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json@@ -13,7 +13,7 @@ "directMessage": "litepub:directMessage" } ], - "id": "http://localhost:8080/followers/fuser3", + "id": "https://remote.org/followers/fuser3", "type": "OrderedCollection", "totalItems": 296 }diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json@@ -13,7 +13,7 @@ "directMessage": "litepub:directMessage" } ], - "id": "http://localhost:8080/following/fuser3", + "id": "https://remote.org/following/fuser3", "type": "OrderedCollection", "totalItems": 32 }diff --git a/test/fixtures/users_mock/masto_closed_followers.json b/test/fixtures/users_mock/masto_closed_followers.json@@ -1,7 +1,7 @@ { "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4001/users/masto_closed/followers", + "id": "https://remote.org/users/masto_closed/followers", "type": "OrderedCollection", "totalItems": 437, - "first": "http://localhost:4001/users/masto_closed/followers?page=1" + "first": "https://remote.org/users/masto_closed/followers?page=1" }diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json@@ -1 +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"]} +{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"https://remote.org/users/masto_closed/followers?page=2","partOf":"https://remote.org/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.json b/test/fixtures/users_mock/masto_closed_following.json@@ -1,7 +1,7 @@ { "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4001/users/masto_closed/following", + "id": "https://remote.org/users/masto_closed/following", "type": "OrderedCollection", "totalItems": 152, - "first": "http://localhost:4001/users/masto_closed/following?page=1" + "first": "https://remote.org/users/masto_closed/following?page=1" }diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json@@ -1 +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"]} +{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"https://remote.org/users/masto_closed/following?page=2","partOf":"https://remote.org/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/fixtures/users_mock/pleroma_followers.json b/test/fixtures/users_mock/pleroma_followers.json@@ -1,18 +1,18 @@ { "type": "OrderedCollection", "totalItems": 527, - "id": "http://localhost:4001/users/fuser2/followers", + "id": "https://remote.org/users/fuser2/followers", "first": { "type": "OrderedCollectionPage", "totalItems": 527, - "partOf": "http://localhost:4001/users/fuser2/followers", + "partOf": "https://remote.org/users/fuser2/followers", "orderedItems": [], - "next": "http://localhost:4001/users/fuser2/followers?page=2", - "id": "http://localhost:4001/users/fuser2/followers?page=1" + "next": "https://remote.org/users/fuser2/followers?page=2", + "id": "https://remote.org/users/fuser2/followers?page=1" }, "@context": [ "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", + "https://remote.org/schemas/litepub-0.1.jsonld", { "@language": "und" }diff --git a/test/fixtures/users_mock/pleroma_following.json b/test/fixtures/users_mock/pleroma_following.json@@ -1,18 +1,18 @@ { "type": "OrderedCollection", "totalItems": 267, - "id": "http://localhost:4001/users/fuser2/following", + "id": "https://remote.org/users/fuser2/following", "first": { "type": "OrderedCollectionPage", "totalItems": 267, - "partOf": "http://localhost:4001/users/fuser2/following", + "partOf": "https://remote.org/users/fuser2/following", "orderedItems": [], - "next": "http://localhost:4001/users/fuser2/following?page=2", - "id": "http://localhost:4001/users/fuser2/following?page=1" + "next": "https://remote.org/users/fuser2/following?page=2", + "id": "https://remote.org/users/fuser2/following?page=1" }, "@context": [ "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", + "https://remote.org/schemas/litepub-0.1.jsonld", { "@language": "und" }diff --git a/test/fixtures/warnings/otp_version/21.1 b/test/fixtures/warnings/otp_version/21.1@@ -1 +0,0 @@ -21.1 -\ No newline at end of filediff --git a/test/fixtures/warnings/otp_version/22.1 b/test/fixtures/warnings/otp_version/22.1@@ -1 +0,0 @@ -22.1 -\ No newline at end of filediff --git a/test/fixtures/warnings/otp_version/22.4 b/test/fixtures/warnings/otp_version/22.4@@ -1 +0,0 @@ -22.4 -\ No newline at end of filediff --git a/test/fixtures/warnings/otp_version/23.0 b/test/fixtures/warnings/otp_version/23.0@@ -1 +0,0 @@ -23.0 -\ No newline at end of filediff --git a/test/mix/tasks/pleroma/app_test.exs b/test/mix/tasks/pleroma/app_test.exs@@ -42,9 +42,10 @@ defmodule Mix.Tasks.Pleroma.AppTest do test "with errors" do Mix.Tasks.Pleroma.App.run(["create"]) - {:mix_shell, :error, ["Creating failed:"]} - {:mix_shell, :error, ["name: can't be blank"]} - {:mix_shell, :error, ["redirect_uris: can't be blank"]} + + assert_receive {:mix_shell, :error, ["Creating failed:"]} + assert_receive {:mix_shell, :error, ["name: can't be blank"]} + assert_receive {:mix_shell, :error, ["redirect_uris: can't be blank"]} end defp assert_app(name, redirect, scopes) dodiff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs@@ -411,7 +411,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do ["scheduled_activities"], ["schema_migrations"], ["thread_mutes"], - # ["user_follows_hashtag"], # not in pleroma + ["user_follows_hashtag"], # ["user_frontend_setting_profiles"], # not in pleroma ["user_invite_tokens"], ["user_notes"],diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs@@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.DigestTest do setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs@@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing)diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do use Pleroma.DataCase setup do - File.mkdir_p!(tmp_path()) + Pleroma.Backports.mkdir_p!(tmp_path()) on_exit(fn -> File.rm_rf(tmp_path())diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs@@ -62,7 +62,7 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads]) if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do - File.mkdir_p(upload_dir) + Pleroma.Backports.mkdir_p(upload_dir) Path.join([upload_dir, "file.txt"]) |> File.touch()diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs@@ -21,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do import Pleroma.Factory setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs@@ -14,7 +14,7 @@ defmodule Pleroma.ConversationTest do setup_all do: clear_config([:instance, :federating], true) setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/ecto_type/activity_pub/object_validators/content_language_map_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/content_language_map_test.exs@@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMapTest do + use Pleroma.DataCase, async: true + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap + + test "it validates" do + data = %{ + "en-US" => "mew mew", + "en-GB" => "meow meow" + } + + assert {:ok, ^data} = ContentLanguageMap.cast(data) + end + + test "it validates empty strings" do + data = %{ + "en-US" => "mew mew", + "en-GB" => "" + } + + assert {:ok, ^data} = ContentLanguageMap.cast(data) + end + + test "it ignores non-strings within the map" do + data = %{ + "en-US" => "mew mew", + "en-GB" => 123 + } + + assert {:ok, validated_data} = ContentLanguageMap.cast(data) + + assert validated_data == %{"en-US" => "mew mew"} + end + + test "it ignores bad locale codes" do + data = %{ + "en-US" => "mew mew", + "en_GB" => "meow meow", + "en<<#@!$#!@%!GB" => "meow meow" + } + + assert {:ok, validated_data} = ContentLanguageMap.cast(data) + + assert validated_data == %{"en-US" => "mew mew"} + end + + test "it complains with non-map data" do + assert :error = ContentLanguageMap.cast("mew") + assert :error = ContentLanguageMap.cast(["mew"]) + assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}]) + end +enddiff --git a/test/pleroma/ecto_type/activity_pub/object_validators/language_code_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/language_code_test.exs@@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCodeTest do + use Pleroma.DataCase, async: true + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode + + test "it accepts language code" do + text = "pl" + assert {:ok, ^text} = LanguageCode.cast(text) + end + + test "it accepts language code with region" do + text = "pl-PL" + assert {:ok, ^text} = LanguageCode.cast(text) + end + + test "errors for invalid language code" do + assert {:error, :invalid_language} = LanguageCode.cast("ru_RU") + assert {:error, :invalid_language} = LanguageCode.cast(" ") + assert {:error, :invalid_language} = LanguageCode.cast("en-US\n") + end + + test "errors for non-text" do + assert :error == LanguageCode.cast(42) + end +enddiff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs@@ -4,6 +4,7 @@ defmodule Pleroma.Emoji.PackTest do use Pleroma.DataCase + alias Pleroma.Emoji alias Pleroma.Emoji.Pack @emoji_path Path.join( @@ -12,6 +13,9 @@ defmodule Pleroma.Emoji.PackTest do ) setup do + # Reload emoji to ensure a clean state + Emoji.reload() + pack_path = Path.join(@emoji_path, "dump_pack") File.mkdir(pack_path) @@ -53,6 +57,63 @@ defmodule Pleroma.Emoji.PackTest do assert updated_pack.files_count == 5 end + + test "skips existing emojis when adding from zip file", %{pack: pack} do + # First, let's create a test pack with a "bear" emoji + test_pack_path = Path.join(@emoji_path, "test_bear_pack") + Pleroma.Backports.mkdir_p(test_pack_path) + + # Create a pack.json file + File.write!(Path.join(test_pack_path, "pack.json"), """ + { + "files": { "bear": "bear.png" }, + "pack": { + "description": "Bear Pack", "homepage": "https://pleroma.social", + "license": "Test license", "share-files": true + }} + """) + + # Copy a test image to use as the bear emoji + File.cp!( + Path.absname("test/instance_static/emoji/test_pack/blank.png"), + Path.join(test_pack_path, "bear.png") + ) + + # Load the pack to register the "bear" emoji in the global registry + {:ok, _bear_pack} = Pleroma.Emoji.Pack.load_pack("test_bear_pack") + + # Reload emoji to make sure the bear emoji is in the global registry + Emoji.reload() + + # Verify that the bear emoji exists in the global registry + assert Emoji.exist?("bear") + + # Now try to add a zip file that contains an emoji with the same shortcode + file = %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") + } + + {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) + + # Verify that the "bear" emoji was skipped + refute Map.has_key?(updated_pack.files, "bear") + + # Other emojis should be added + assert Map.has_key?(updated_pack.files, "a_trusted_friend-128") + assert Map.has_key?(updated_pack.files, "auroraborealis") + assert Map.has_key?(updated_pack.files, "baby_in_a_box") + assert Map.has_key?(updated_pack.files, "bear-128") + + # Total count should be 4 (all emojis except "bear") + assert updated_pack.files_count == 4 + + # Clean up the test pack + on_exit(fn -> + File.rm_rf!(test_pack_path) + end) + end end test "returns error when zip file is bad", %{pack: pack} do @@ -62,7 +123,7 @@ defmodule Pleroma.Emoji.PackTest do path: Path.absname("test/instance_static/emoji/test_pack/blank.png") } - assert Pack.add_file(pack, nil, nil, file) == {:error, :einval} + assert {:error, _} = Pack.add_file(pack, nil, nil, file) end test "returns pack when zip file is empty", %{pack: pack} dodiff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs@@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing)diff --git a/test/pleroma/hashtag_test.exs b/test/pleroma/hashtag_test.exs@@ -14,4 +14,133 @@ defmodule Pleroma.HashtagTest do assert {:name, {"can't be blank", [validation: :required]}} in changeset.errors end end + + describe "search_hashtags" do + test "searches hashtags by partial match" do + {:ok, _} = Hashtag.get_or_create_by_name("car") + {:ok, _} = Hashtag.get_or_create_by_name("racecar") + {:ok, _} = Hashtag.get_or_create_by_name("nascar") + {:ok, _} = Hashtag.get_or_create_by_name("bicycle") + + results = Hashtag.search("car") + assert "car" in results + assert "racecar" in results + assert "nascar" in results + refute "bicycle" in results + + results = Hashtag.search("race") + assert "racecar" in results + refute "car" in results + refute "nascar" in results + refute "bicycle" in results + + results = Hashtag.search("nonexistent") + assert results == [] + end + + test "searches hashtags by multiple words in query" do + {:ok, _} = Hashtag.get_or_create_by_name("computer") + {:ok, _} = Hashtag.get_or_create_by_name("laptop") + {:ok, _} = Hashtag.get_or_create_by_name("desktop") + {:ok, _} = Hashtag.get_or_create_by_name("phone") + + # Search for "new computer" - should return "computer" + results = Hashtag.search("new computer") + assert "computer" in results + refute "laptop" in results + refute "desktop" in results + refute "phone" in results + + # Search for "computer laptop" - should return both + results = Hashtag.search("computer laptop") + assert "computer" in results + assert "laptop" in results + refute "desktop" in results + refute "phone" in results + + # Search for "new phone" - should return "phone" + results = Hashtag.search("new phone") + assert "phone" in results + refute "computer" in results + refute "laptop" in results + refute "desktop" in results + end + + test "supports pagination" do + {:ok, _} = Hashtag.get_or_create_by_name("alpha") + {:ok, _} = Hashtag.get_or_create_by_name("beta") + {:ok, _} = Hashtag.get_or_create_by_name("gamma") + {:ok, _} = Hashtag.get_or_create_by_name("delta") + + results = Hashtag.search("a", limit: 2) + assert length(results) == 2 + + results = Hashtag.search("a", limit: 2, offset: 1) + assert length(results) == 2 + end + + test "handles matching many search terms" do + {:ok, _} = Hashtag.get_or_create_by_name("computer") + {:ok, _} = Hashtag.get_or_create_by_name("laptop") + {:ok, _} = Hashtag.get_or_create_by_name("phone") + {:ok, _} = Hashtag.get_or_create_by_name("tablet") + + results = Hashtag.search("new fast computer laptop phone tablet device") + assert "computer" in results + assert "laptop" in results + assert "phone" in results + assert "tablet" in results + end + + test "ranks results by match quality" do + {:ok, _} = Hashtag.get_or_create_by_name("my_computer") + {:ok, _} = Hashtag.get_or_create_by_name("computer_science") + {:ok, _} = Hashtag.get_or_create_by_name("computer") + + results = Hashtag.search("computer") + + # Exact match first + assert Enum.at(results, 0) == "computer" + + # Prefix match would be next + assert Enum.at(results, 1) == "computer_science" + + # worst match is last + assert Enum.at(results, 2) == "my_computer" + end + + test "prioritizes shorter names when ranking is equal" do + # Create hashtags with same ranking but different lengths + {:ok, _} = Hashtag.get_or_create_by_name("car") + {:ok, _} = Hashtag.get_or_create_by_name("racecar") + {:ok, _} = Hashtag.get_or_create_by_name("nascar") + + # Search for "car" - shorter names should come first + results = Hashtag.search("car") + # Shortest exact match first + assert Enum.at(results, 0) == "car" + assert "racecar" in results + assert "nascar" in results + end + + test "handles hashtag symbols in search query" do + {:ok, _} = Hashtag.get_or_create_by_name("computer") + {:ok, _} = Hashtag.get_or_create_by_name("laptop") + {:ok, _} = Hashtag.get_or_create_by_name("phone") + + results_with_hash = Hashtag.search("#computer #laptop") + results_without_hash = Hashtag.search("computer laptop") + + assert results_with_hash == results_without_hash + + results_mixed = Hashtag.search("#computer laptop #phone") + assert "computer" in results_mixed + assert "laptop" in results_mixed + assert "phone" in results_mixed + + results_only_hash = Hashtag.search("#computer") + results_no_hash = Hashtag.search("computer") + assert results_only_hash == results_no_hash + end + end enddiff --git a/test/pleroma/http_test.exs b/test/pleroma/http_test.exs@@ -25,6 +25,9 @@ defmodule Pleroma.HTTPTest do %{method: :post, url: "http://example.com/world"} -> %Tesla.Env{status: 200, body: "world"} + + %{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} -> + %Tesla.Env{status: 200, body: "emoji data"} end) :ok @@ -67,4 +70,20 @@ defmodule Pleroma.HTTPTest do } end end + + test "URL encoding properly encodes URLs with spaces" do + clear_config(:test_url_encoding, true) + + url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz" + + {:ok, result} = HTTP.get(url_with_space) + + assert result.status == 200 + + properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz" + + {:ok, result} = HTTP.get(properly_encoded_url) + + assert result.status == 200 + end enddiff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs@@ -3,19 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Instances.InstanceTest do - alias Pleroma.Instances alias Pleroma.Instances.Instance alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI + use Oban.Testing, repo: Pleroma.Repo use Pleroma.DataCase import ExUnit.CaptureLog import Pleroma.Factory - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - describe "set_reachable/1" do test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) @@ -31,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do assert {:ok, instance} = Instance.set_reachable(instance.host) refute instance.unreachable_since end + + test "cancels all ReachabilityWorker jobs for the domain" do + domain = "cancelme.example.org" + insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now()) + + # Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future + scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second) + + {:ok, job} = + Pleroma.Workers.ReachabilityWorker.new( + %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1}, + scheduled_at: scheduled_at + ) + |> Oban.insert() + + # Ensure the job is present + job = Pleroma.Repo.get(Oban.Job, job.id) + assert job + + # Call set_reachable, which should delete the job + assert {:ok, _} = Instance.set_reachable(domain) + + # Reload the job and assert it is deleted + job = Pleroma.Repo.get(Oban.Job, job.id) + refute job + end end describe "set_unreachable/1" do @@ -145,7 +167,11 @@ defmodule Pleroma.Instances.InstanceTest do end test "Doesn't scrapes unreachable instances" do - instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold()) + instance = + insert(:instance, + unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24)) + ) + url = "https://" <> instance.host assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~ @@ -213,32 +239,44 @@ defmodule Pleroma.Instances.InstanceTest do end end - test "delete_users_and_activities/1 deletes remote instance users and activities" do - [mario, luigi, _peach, wario] = - users = [ - insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"), - insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"), - insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"), - insert(:user, nickname: "wario@greedville.biz", name: "Wario") - ] + test "delete/1 schedules a job to delete the instance and users" do + insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario") + + {:ok, _job} = Instance.delete("mushroom.kingdom") + + assert_enqueued( + worker: Pleroma.Workers.DeleteWorker, + args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"} + ) + end + + describe "check_unreachable/1" do + test "schedules a ReachabilityWorker job for the given domain" do + domain = "test.example.com" - {:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"}) - {:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"}) - {:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"}) + # Call check_unreachable + assert {:ok, _job} = Instance.check_unreachable(domain) - {:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom") - :ok = ObanHelpers.perform(job) + # Verify that a ReachabilityWorker job was scheduled + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == domain + end - [mario, luigi, peach, wario] = Repo.reload(users) + test "handles multiple calls for the same domain (uniqueness enforced)" do + domain = "duplicate.example.com" - refute mario.is_active - refute luigi.is_active - refute peach.is_active - refute peach.name == "Peach" + assert {:ok, _job1} = Instance.check_unreachable(domain) - assert wario.is_active - assert wario.name == "Wario" + # Second call for the same domain + assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain) - assert [nil, nil, %{}] = Repo.reload([post1, post2, post3]) + # Should only have one job due to uniqueness + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == domain + end end enddiff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs@@ -6,74 +6,42 @@ defmodule Pleroma.InstancesTest do alias Pleroma.Instances use Pleroma.DataCase - - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) + use Oban.Testing, repo: Pleroma.Repo describe "reachable?/1" do test "returns `true` for host / url with unknown reachability status" do assert Instances.reachable?("unknown.site") assert Instances.reachable?("http://unknown.site") end - - test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do - host = "consistently-unreachable.name" - Instances.set_consistently_unreachable(host) - - refute Instances.reachable?(host) - refute Instances.reachable?("http://#{host}/path") - end - - test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do - url = "http://eventually-unreachable.name/path" - - Instances.set_unreachable(url) - - assert Instances.reachable?(url) - assert Instances.reachable?(URI.parse(url).host) - end - - test "raises FunctionClauseError exception on non-binary input" do - assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end - assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end - end end describe "filter_reachable/1" do setup do - host = "consistently-unreachable.name" - url1 = "http://eventually-unreachable.com/path" - url2 = "http://domain.com/path" + unreachable_host = "consistently-unreachable.name" + reachable_host = "http://domain.com/path" - Instances.set_consistently_unreachable(host) - Instances.set_unreachable(url1) + Instances.set_unreachable(unreachable_host) - result = Instances.filter_reachable([host, url1, url2, nil]) - %{result: result, url1: url1, url2: url2} + result = Instances.filter_reachable([unreachable_host, reachable_host, nil]) + %{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host} end - test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() + test "returns a list of only reachable elements", + %{result: result, reachable_host: reachable_host} do + assert is_list(result) + assert [reachable_host] == result end - test "returns a map with `unreachable_since` values for keys", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert %NaiveDateTime{} = result[url1] - assert is_nil(result[url2]) - end - - test "returns an empty map for empty list or list containing no hosts / url" do - assert %{} == Instances.filter_reachable([]) - assert %{} == Instances.filter_reachable([nil]) + test "returns an empty list when provided no data" do + assert [] == Instances.filter_reachable([]) + assert [] == Instances.filter_reachable([nil]) end end describe "set_reachable/1" do test "sets unreachable url or host reachable" do host = "domain.com" - Instances.set_consistently_unreachable(host) + Instances.set_unreachable(host) refute Instances.reachable?(host) Instances.set_reachable(host) @@ -103,22 +71,68 @@ defmodule Pleroma.InstancesTest do end end - describe "set_consistently_unreachable/1" do - test "sets reachable url or host unreachable" do - url = "http://domain.com?q=" - assert Instances.reachable?(url) + describe "check_all_unreachable/0" do + test "schedules ReachabilityWorker jobs for all unreachable instances" do + domain1 = "unreachable1.example.com" + domain2 = "unreachable2.example.com" + domain3 = "unreachable3.example.com" - Instances.set_consistently_unreachable(url) - refute Instances.reachable?(url) + Instances.set_unreachable(domain1) + Instances.set_unreachable(domain2) + Instances.set_unreachable(domain3) + + Instances.check_all_unreachable() + + # Verify that ReachabilityWorker jobs were scheduled for all unreachable domains + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 3 + + domains = Enum.map(jobs, & &1.args["domain"]) + assert domain1 in domains + assert domain2 in domains + assert domain3 in domains end - test "keeps unreachable url or host unreachable" do - host = "site.name" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) + test "does not schedule jobs for reachable instances" do + unreachable_domain = "unreachable.example.com" + reachable_domain = "reachable.example.com" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) + Instances.set_unreachable(unreachable_domain) + Instances.set_reachable(reachable_domain) + + Instances.check_all_unreachable() + + # Verify that only one job was scheduled (for the unreachable domain) + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == unreachable_domain end end + + test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do + domain1 = "unreachable1.example.com" + domain2 = "unreachable2.example.com" + domain3 = "unreachable3.example.com" + + Instances.set_unreachable(domain1) + Instances.set_unreachable(domain2) + Instances.set_unreachable(domain3) + + Instances.delete_all_unreachable() + + # Verify that DeleteWorker jobs were scheduled for all unreachable domains + jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker) + assert length(jobs) == 3 + + domains = Enum.map(jobs, & &1.args["host"]) + assert domain1 in domains + assert domain2 in domains + assert domain3 in domains + + # Verify all jobs are delete_instance operations + Enum.each(jobs, fn job -> + assert job.args["op"] == "delete_instance" + end) + end enddiff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs@@ -268,6 +268,17 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do end) end + test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do + assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}]) + + capture_log(fn -> + assert {:error, %WebSockex.RequestError{code: 401}} = + start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) + + Process.sleep(30) + end) + end + test "accepts valid token on client-sent event", %{token: token} do assert {:ok, pid} = start_socket()diff --git a/test/pleroma/language/language_detector_test.exs b/test/pleroma/language/language_detector_test.exs@@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetectorTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Language.LanguageDetector + alias Pleroma.Language.LanguageDetectorMock + alias Pleroma.StaticStubbedConfigMock + + import Mox + + setup do + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + # Stub the LanguageDetectorMock with default implementations + LanguageDetectorMock + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + + :ok + end + + test "it detects text language" do + LanguageDetectorMock + |> expect(:detect, fn _text -> "fr" end) + + detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.") + + assert detected_language == "fr" + end + + test "it returns nil if text is not long enough" do + # No need to set expectations as the word count check happens before the provider is called + + detected_language = LanguageDetector.detect("it returns nil") + + assert detected_language == nil + end + + test "it returns nil if no provider specified" do + # Override the stub to return nil for the provider + StaticStubbedConfigMock + |> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end) + + detected_language = LanguageDetector.detect("this should also return nil") + + assert detected_language == nil + end +enddiff --git a/test/pleroma/language/translation/deepl_test.exs b/test/pleroma/language/translation/deepl_test.exs@@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.DeeplTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Language.Translation.Deepl + + test "it translates text" do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com") + clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY") + + {:ok, res} = + Deepl.translate( + "USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!", + "pl", + "en" + ) + + assert %{ + detected_source_language: "PL", + provider: "DeepL" + } = res + end + + test "it returns languages list" do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com") + clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY") + + assert {:ok, [language | _languages]} = Deepl.supported_languages(:target) + + assert is_binary(language) + end +enddiff --git a/test/pleroma/language/translation_test.exs b/test/pleroma/language/translation_test.exs@@ -0,0 +1,28 @@ +defmodule Pleroma.Language.TranslationTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Language.Translation + + setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + test "it translates text" do + assert {:ok, + %{ + content: "txet emos", + detected_source_language: _, + provider: _ + }} = Translation.translate("some text", "en", "uk") + end + + test "it stores translation result in cache" do + Translation.translate("some text", "en", "uk") + + assert {:ok, result} = + Cachex.get( + :translations_cache, + "en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}" + ) + + assert result.content == "txet emos" + end +enddiff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs@@ -19,7 +19,7 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.MastodonAPI.NotificationView setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs@@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do use Pleroma.DataCase alias Pleroma.Activity - alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Object.Fetcher alias Pleroma.Web.ActivityPub.ObjectValidator @@ -166,15 +165,89 @@ defmodule Pleroma.Object.FetcherTest do ) end - test "it resets instance reachability on successful fetch" do - id = "http://mastodon.example.org/@admin/99541947525187367" - Instances.set_consistently_unreachable(id) - refute Instances.reachable?(id) + test "it does not fetch from local instance" do + local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource" - {:ok, _object} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + assert {:fetch, {:error, "Trying to fetch local resource"}} = + Fetcher.fetch_object_from_id(local_url) + end - assert Instances.reachable?(id) + test "it validates content-type headers according to ActivityPub spec" do + # Setup a mock for an object with invalid content-type + mock(fn + %{method: :get, url: "https://example.com/objects/invalid-content-type"} -> + %Tesla.Env{ + status: 200, + # Not a valid AP content-type + headers: [{"content-type", "application/json"}], + body: + Jason.encode!(%{ + "id" => "https://example.com/objects/invalid-content-type", + "type" => "Note", + "content" => "This has an invalid content type", + "actor" => "https://example.com/users/actor", + "attributedTo" => "https://example.com/users/actor" + }) + } + end) + + assert {:fetch, {:error, {:content_type, "application/json"}}} = + Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type") + end + + test "it accepts objects with application/ld+json and ActivityStreams profile" do + # Setup a mock for an object with ld+json content-type and AS profile + mock(fn + %{method: :get, url: "https://example.com/objects/valid-ld-json"} -> + %Tesla.Env{ + status: 200, + headers: [ + {"content-type", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""} + ], + body: + Jason.encode!(%{ + "id" => "https://example.com/objects/valid-ld-json", + "type" => "Note", + "content" => "This has a valid ld+json content type", + "actor" => "https://example.com/users/actor", + "attributedTo" => "https://example.com/users/actor" + }) + } + end) + + # This should pass if content-type validation works correctly + assert {:ok, object} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://example.com/objects/valid-ld-json" + ) + + assert object["content"] == "This has a valid ld+json content type" + end + + test "it rejects objects with no content-type header" do + # Setup a mock for an object with no content-type header + mock(fn + %{method: :get, url: "https://example.com/objects/no-content-type"} -> + %Tesla.Env{ + status: 200, + # No content-type header + headers: [], + body: + Jason.encode!(%{ + "id" => "https://example.com/objects/no-content-type", + "type" => "Note", + "content" => "This has no content type header", + "actor" => "https://example.com/users/actor", + "attributedTo" => "https://example.com/users/actor" + }) + } + end) + + # We want to test that the request fails with a missing content-type error + # but the actual error is {:fetch, {:error, nil}} - we'll check for this format + result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type") + assert {:fetch, {:error, nil}} = result end end @@ -534,6 +607,110 @@ defmodule Pleroma.Object.FetcherTest do end end + describe "cross-domain redirect handling" do + setup do + mock(fn + # Cross-domain redirect with original domain in id + %{method: :get, url: "https://original.test/objects/123"} -> + %Tesla.Env{ + status: 200, + url: "https://media.test/objects/123", + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://original.test/objects/123", + "type" => "Note", + "content" => "This is redirected content", + "actor" => "https://original.test/users/actor", + "attributedTo" => "https://original.test/users/actor" + }) + } + + # Cross-domain redirect with final domain in id + %{method: :get, url: "https://original.test/objects/final-domain-id"} -> + %Tesla.Env{ + status: 200, + url: "https://media.test/objects/final-domain-id", + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://media.test/objects/final-domain-id", + "type" => "Note", + "content" => "This has final domain in id", + "actor" => "https://original.test/users/actor", + "attributedTo" => "https://original.test/users/actor" + }) + } + + # No redirect - same domain + %{method: :get, url: "https://original.test/objects/same-domain-redirect"} -> + %Tesla.Env{ + status: 200, + url: "https://original.test/objects/different-path", + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://original.test/objects/same-domain-redirect", + "type" => "Note", + "content" => "This has a same-domain redirect", + "actor" => "https://original.test/users/actor", + "attributedTo" => "https://original.test/users/actor" + }) + } + + # Test case with missing url field in response (common in tests) + %{method: :get, url: "https://original.test/objects/missing-url"} -> + %Tesla.Env{ + status: 200, + # No url field + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://original.test/objects/missing-url", + "type" => "Note", + "content" => "This has no URL field in response", + "actor" => "https://original.test/users/actor", + "attributedTo" => "https://original.test/users/actor" + }) + } + end) + + :ok + end + + test "it rejects objects from cross-domain redirects with original domain in id" do + assert {:error, {:cross_domain_redirect, true}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://original.test/objects/123" + ) + end + + test "it rejects objects from cross-domain redirects with final domain in id" do + assert {:error, {:cross_domain_redirect, true}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://original.test/objects/final-domain-id" + ) + end + + test "it accepts objects with same-domain redirects" do + assert {:ok, data} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://original.test/objects/same-domain-redirect" + ) + + assert data["content"] == "This has a same-domain redirect" + end + + test "it handles responses without URL field (common in tests)" do + assert {:ok, data} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://original.test/objects/missing-url" + ) + + assert data["content"] == "This has no URL field in response" + end + end + describe "fetch with history" do setup do object2 = %{diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs@@ -6,12 +6,10 @@ defmodule Pleroma.ObjectTest do use Pleroma.DataCase use Oban.Testing, repo: Pleroma.Repo - import ExUnit.CaptureLog import Mox import Pleroma.Factory import Tesla.Mock - alias Pleroma.Activity alias Pleroma.Hashtag alias Pleroma.Object alias Pleroma.Repo @@ -158,7 +156,7 @@ defmodule Pleroma.ObjectTest do uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - File.mkdir_p!(uploads_dir) + Pleroma.Backports.mkdir_p!(uploads_dir) file = %Plug.Upload{ content_type: "image/jpeg", @@ -176,8 +174,9 @@ defmodule Pleroma.ObjectTest do filename = Path.basename(href) - assert {:ok, files} = File.ls(uploads_dir) - assert filename in files + expected_path = Path.join([uploads_dir, Pleroma.Upload.Filter.Dedupe.shard_path(filename)]) + + assert File.exists?(expected_path) Object.delete(note) @@ -185,8 +184,7 @@ defmodule Pleroma.ObjectTest do assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil - assert {:ok, files} = File.ls(uploads_dir) - refute filename in files + refute File.exists?(expected_path) end test "with objects that have legacy data.url attribute" do @@ -282,148 +280,6 @@ 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"), - headers: HttpRequestMock.activitypub_object_headers() - } - - 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", - fetch: true - ) - - 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"), - headers: HttpRequestMock.activitypub_object_headers() - }) - - 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", - fetch: true - ) - - 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", - fetch: true - ) - - 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"), - headers: HttpRequestMock.activitypub_object_headers() - }) - - 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", - fetch: true - ) - - 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} = CommonAPI.favorite(activity.id, user) - object = Object.get_by_ap_id(activity.data["object"]) - - assert object.data["like_count"] == 1 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json"), - headers: HttpRequestMock.activitypub_object_headers() - }) - - 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 - describe ":hashtags association" do test "Hashtag records are created with Object record and updated on its change" do user = insert(:user)diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs@@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersionTest do - use ExUnit.Case, async: true - - alias Pleroma.OTPVersion - - describe "check/1" do - test "22.4" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == - "22.4" - end - - test "22.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == - "22.1" - end - - test "21.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == - "21.1" - end - - test "23.0" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == - "23.0" - end - - test "with nonexistent file" do - assert OTPVersion.get_version_from_files([ - "test/fixtures/warnings/otp_version/non-exising", - "test/fixtures/warnings/otp_version/22.4" - ]) == "22.4" - end - - test "empty paths" do - assert OTPVersion.get_version_from_files([]) == nil - end - end -enddiff --git a/test/pleroma/release_task_test.exs b/test/pleroma/release_task_test.exs@@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReleaseTaskTest do + use Pleroma.DataCase, async: true + + alias Pleroma.ReleaseTasks + + test "finding the module" do + task = "search.meilisearch" + assert Mix.Tasks.Pleroma.Search.Meilisearch == ReleaseTasks.find_module(task) + + task = "user" + assert Mix.Tasks.Pleroma.User == ReleaseTasks.find_module(task) + + refute ReleaseTasks.find_module("doesnt.exist") + end +enddiff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs@@ -3,12 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true import Pleroma.Factory import Pleroma.Tests.Helpers alias Pleroma.ConfigDB - setup do: clear_config(Pleroma.Formatter) setup_all do: require_migration("20200716195806_autolinker_to_linkify") test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} dodiff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs@@ -63,7 +63,11 @@ defmodule Pleroma.ReverseProxyTest do |> Plug.Conn.put_req_header("user-agent", "fake/1.0") |> ReverseProxy.call("/user-agent") - assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()} + # Convert the response to a map without relying on json_response + body = conn.resp_body + assert conn.status == 200 + response = Jason.decode!(body) + assert response == %{"user-agent" => Pleroma.Application.user_agent()} end test "closed connection", %{conn: conn} do @@ -138,11 +142,14 @@ defmodule Pleroma.ReverseProxyTest do test "common", %{conn: conn} do ClientMock |> expect(:request, fn :head, "/head", _, _, _ -> - {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]} + {:ok, 200, [{"content-type", "image/png"}]} end) conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") - assert html_response(conn, 200) == "" + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["image/png"] + assert conn.resp_body == "" end end @@ -249,7 +256,10 @@ defmodule Pleroma.ReverseProxyTest do ) |> ReverseProxy.call("/headers") - %{"headers" => headers} = json_response(conn, 200) + body = conn.resp_body + assert conn.status == 200 + response = Jason.decode!(body) + headers = response["headers"] assert headers["Accept"] == "text/html" end @@ -262,7 +272,10 @@ defmodule Pleroma.ReverseProxyTest do ) |> ReverseProxy.call("/headers") - %{"headers" => headers} = json_response(conn, 200) + body = conn.resp_body + assert conn.status == 200 + response = Jason.decode!(body) + headers = response["headers"] refute headers["Accept-Language"] end end @@ -328,4 +341,94 @@ defmodule Pleroma.ReverseProxyTest do assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers end end + + describe "content-type sanitisation" do + test "preserves allowed image type", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/content", _, _, _ -> + {:ok, 200, [{"content-type", "image/png"}], %{url: "/content"}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["image/png"] + end + + test "preserves allowed video type", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/content", _, _, _ -> + {:ok, 200, [{"content-type", "video/mp4"}], %{url: "/content"}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"] + end + + test "sanitizes ActivityPub content type", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/content", _, _, _ -> + {:ok, 200, [{"content-type", "application/activity+json"}], %{url: "/content"}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + end + + test "sanitizes LD-JSON content type", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/content", _, _, _ -> + {:ok, 200, [{"content-type", "application/ld+json"}], %{url: "/content"}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + end + end + + # Hackey is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter + # Gun is able to proxy through Tesla, so it does not need testing as the + # test cases in the Pleroma.HTTPTest module are sufficient + describe "Hackney URL encoding:" do + setup do + ClientMock + |> expect(:request, fn :get, + "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz", + _headers, + _body, + _opts -> + {:ok, 200, [{"content-type", "image/png"}], "It works!"} + end) + |> stub(:stream_body, fn _ -> :done end) + |> stub(:close, fn _ -> :ok end) + + :ok + end + + test "properly encodes URLs with spaces", %{conn: conn} do + url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz" + + result = ReverseProxy.call(conn, url_with_space) + + assert result.status == 200 + end + + test "properly encoded URL should not be altered", %{conn: conn} do + properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz" + + result = ReverseProxy.call(conn, properly_encoded_url) + + assert result.status == 200 + end + end enddiff --git a/test/pleroma/safe_zip_test.exs b/test/pleroma/safe_zip_test.exs@@ -0,0 +1,496 @@ +defmodule Pleroma.SafeZipTest do + # Not making this async because it creates and deletes files + use ExUnit.Case + + alias Pleroma.SafeZip + + @fixtures_dir "test/fixtures" + @tmp_dir "test/zip_tmp" + + setup do + # Ensure tmp directory exists + Pleroma.Backports.mkdir_p!(@tmp_dir) + + on_exit(fn -> + # Clean up any files created during tests + File.rm_rf!(@tmp_dir) + Pleroma.Backports.mkdir_p!(@tmp_dir) + end) + + :ok + end + + describe "list_dir_file/1" do + test "lists files in a valid zip" do + {:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "emojis.zip")) + assert is_list(files) + assert length(files) > 0 + end + + test "returns an empty list for empty zip" do + {:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "empty.zip")) + assert files == [] + end + + test "returns error for non-existent file" do + assert {:error, _} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "nonexistent.zip")) + end + + test "only lists regular files, not directories" do + # Create a zip with both files and directories + zip_path = create_zip_with_directory() + + # List files with SafeZip + {:ok, files} = SafeZip.list_dir_file(zip_path) + + # Verify only regular files are listed, not directories + assert "file_in_dir/test_file.txt" in files + assert "root_file.txt" in files + + # Directory entries should not be included in the list + refute "file_in_dir/" in files + end + end + + describe "contains_all_data?/2" do + test "returns true when all files are in the archive" do + # For this test, we'll create our own zip file with known content + # to ensure we can test the contains_all_data? function properly + zip_path = create_zip_with_directory() + archive_data = File.read!(zip_path) + + # Check if the archive contains the root file + # Note: The function expects charlists (Erlang strings) in the MapSet + assert SafeZip.contains_all_data?(archive_data, MapSet.new([~c"root_file.txt"])) + end + + test "returns false when files are missing" do + archive_path = Path.join(@fixtures_dir, "emojis.zip") + archive_data = File.read!(archive_path) + + # Create a MapSet with non-existent files + fset = MapSet.new([~c"nonexistent.txt"]) + + refute SafeZip.contains_all_data?(archive_data, fset) + end + + test "returns false for invalid archive data" do + refute SafeZip.contains_all_data?("invalid data", MapSet.new([~c"file.txt"])) + end + + test "only checks for regular files, not directories" do + # Create a zip with both files and directories + zip_path = create_zip_with_directory() + archive_data = File.read!(zip_path) + + # Check if the archive contains a directory (should return false) + refute SafeZip.contains_all_data?(archive_data, MapSet.new([~c"file_in_dir/"])) + + # For this test, we'll manually check if the file exists in the archive + # by extracting it and verifying it exists + extract_dir = Path.join(@tmp_dir, "extract_check") + Pleroma.Backports.mkdir_p!(extract_dir) + {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir) + + # Verify the root file was extracted + assert Enum.any?(files, fn file -> + Path.basename(file) == "root_file.txt" + end) + + # Verify the file exists on disk + assert File.exists?(Path.join(extract_dir, "root_file.txt")) + end + end + + describe "zip/4" do + test "creates a zip file on disk" do + # Create a test file + test_file_path = Path.join(@tmp_dir, "test_file.txt") + File.write!(test_file_path, "test content") + + # Create a zip file + zip_path = Path.join(@tmp_dir, "test.zip") + assert {:ok, ^zip_path} = SafeZip.zip(zip_path, ["test_file.txt"], @tmp_dir, false) + + # Verify the zip file exists + assert File.exists?(zip_path) + end + + test "creates a zip file in memory" do + # Create a test file + test_file_path = Path.join(@tmp_dir, "test_file.txt") + File.write!(test_file_path, "test content") + + # Create a zip file in memory + zip_name = Path.join(@tmp_dir, "test.zip") + + assert {:ok, {^zip_name, zip_data}} = + SafeZip.zip(zip_name, ["test_file.txt"], @tmp_dir, true) + + # Verify the zip data is binary + assert is_binary(zip_data) + end + + test "returns error for unsafe paths" do + # Try to zip a file with path traversal + assert {:error, _} = + SafeZip.zip( + Path.join(@tmp_dir, "test.zip"), + ["../fixtures/test.txt"], + @tmp_dir, + false + ) + end + + test "can create zip with directories" do + # Create a directory structure + dir_path = Path.join(@tmp_dir, "test_dir") + Pleroma.Backports.mkdir_p!(dir_path) + + file_in_dir_path = Path.join(dir_path, "file_in_dir.txt") + File.write!(file_in_dir_path, "file in directory") + + # Create a zip file + zip_path = Path.join(@tmp_dir, "dir_test.zip") + + assert {:ok, ^zip_path} = + SafeZip.zip( + zip_path, + ["test_dir/file_in_dir.txt"], + @tmp_dir, + false + ) + + # Verify the zip file exists + assert File.exists?(zip_path) + + # Extract and verify the directory structure is preserved + extract_dir = Path.join(@tmp_dir, "extract") + {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir) + + # Check if the file path is in the list, accounting for possible full paths + assert Enum.any?(files, fn file -> + String.ends_with?(file, "file_in_dir.txt") + end) + + # Verify the file exists in the expected location + assert File.exists?(Path.join([extract_dir, "test_dir", "file_in_dir.txt"])) + end + end + + describe "unzip_file/3" do + test "extracts files from a zip archive" do + archive_path = Path.join(@fixtures_dir, "emojis.zip") + + # Extract the archive + assert {:ok, files} = SafeZip.unzip_file(archive_path, @tmp_dir) + + # Verify files were extracted + assert is_list(files) + assert length(files) > 0 + + # Verify at least one file exists + first_file = List.first(files) + + # Simply check that the file exists in the tmp directory + assert File.exists?(first_file) + end + + test "extracts specific files from a zip archive" do + archive_path = Path.join(@fixtures_dir, "emojis.zip") + + # Get list of files in the archive + {:ok, all_files} = SafeZip.list_dir_file(archive_path) + file_to_extract = List.first(all_files) + + # Extract only one file + assert {:ok, [extracted_file]} = + SafeZip.unzip_file(archive_path, @tmp_dir, [file_to_extract]) + + # Verify only the specified file was extracted + assert Path.basename(extracted_file) == Path.basename(file_to_extract) + + # Check that the file exists in the tmp directory + assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract))) + end + + test "returns error for invalid zip file" do + invalid_path = Path.join(@tmp_dir, "invalid.zip") + File.write!(invalid_path, "not a zip file") + + assert {:error, _} = SafeZip.unzip_file(invalid_path, @tmp_dir) + end + + test "creates directories when extracting files in subdirectories" do + # Create a zip with files in subdirectories + zip_path = create_zip_with_directory() + + # Extract the archive + assert {:ok, files} = SafeZip.unzip_file(zip_path, @tmp_dir) + + # Verify files were extracted - handle both relative and absolute paths + assert Enum.any?(files, fn file -> + Path.basename(file) == "test_file.txt" && + String.contains?(file, "file_in_dir") + end) + + assert Enum.any?(files, fn file -> + Path.basename(file) == "root_file.txt" + end) + + # Verify directory was created + dir_path = Path.join(@tmp_dir, "file_in_dir") + assert File.exists?(dir_path) + assert File.dir?(dir_path) + + # Verify file in directory was extracted + file_path = Path.join(dir_path, "test_file.txt") + assert File.exists?(file_path) + end + end + + describe "unzip_data/3" do + test "extracts files from zip data" do + archive_path = Path.join(@fixtures_dir, "emojis.zip") + archive_data = File.read!(archive_path) + + # Extract the archive from data + assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir) + + # Verify files were extracted + assert is_list(files) + assert length(files) > 0 + + # Verify at least one file exists + first_file = List.first(files) + + # Simply check that the file exists in the tmp directory + assert File.exists?(first_file) + end + + test "extracts specific files from zip data" do + archive_path = Path.join(@fixtures_dir, "emojis.zip") + archive_data = File.read!(archive_path) + + # Get list of files in the archive + {:ok, all_files} = SafeZip.list_dir_file(archive_path) + file_to_extract = List.first(all_files) + + # Extract only one file + assert {:ok, extracted_files} = + SafeZip.unzip_data(archive_data, @tmp_dir, [file_to_extract]) + + # Verify only the specified file was extracted + assert Enum.any?(extracted_files, fn path -> + Path.basename(path) == Path.basename(file_to_extract) + end) + + # Simply check that the file exists in the tmp directory + assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract))) + end + + test "returns error for invalid zip data" do + assert {:error, _} = SafeZip.unzip_data("not a zip file", @tmp_dir) + end + + test "creates directories when extracting files in subdirectories from data" do + # Create a zip with files in subdirectories + zip_path = create_zip_with_directory() + archive_data = File.read!(zip_path) + + # Extract the archive from data + assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir) + + # Verify files were extracted - handle both relative and absolute paths + assert Enum.any?(files, fn file -> + Path.basename(file) == "test_file.txt" && + String.contains?(file, "file_in_dir") + end) + + assert Enum.any?(files, fn file -> + Path.basename(file) == "root_file.txt" + end) + + # Verify directory was created + dir_path = Path.join(@tmp_dir, "file_in_dir") + assert File.exists?(dir_path) + assert File.dir?(dir_path) + + # Verify file in directory was extracted + file_path = Path.join(dir_path, "test_file.txt") + assert File.exists?(file_path) + end + end + + # Security tests + describe "security checks" do + test "prevents path traversal in zip extraction" do + # Create a malicious zip file with path traversal + malicious_zip_path = create_malicious_zip_with_path_traversal() + + # Try to extract it with SafeZip + assert {:error, _} = SafeZip.unzip_file(malicious_zip_path, @tmp_dir) + + # Verify the file was not extracted outside the target directory + refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt")) + end + + test "prevents directory traversal in zip listing" do + # Create a malicious zip file with path traversal + malicious_zip_path = create_malicious_zip_with_path_traversal() + + # Try to list files with SafeZip + assert {:error, _} = SafeZip.list_dir_file(malicious_zip_path) + end + + test "prevents path traversal in zip data extraction" do + # Create a malicious zip file with path traversal + malicious_zip_path = create_malicious_zip_with_path_traversal() + malicious_data = File.read!(malicious_zip_path) + + # Try to extract it with SafeZip + assert {:error, _} = SafeZip.unzip_data(malicious_data, @tmp_dir) + + # Verify the file was not extracted outside the target directory + refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt")) + end + + test "handles zip bomb attempts" do + # Create a zip bomb (a zip with many files or large files) + zip_bomb_path = create_zip_bomb() + + # The SafeZip module should handle this gracefully + # Either by successfully extracting it (if it's not too large) + # or by returning an error (if it detects a potential zip bomb) + result = SafeZip.unzip_file(zip_bomb_path, @tmp_dir) + + case result do + {:ok, _} -> + # If it successfully extracts, make sure it didn't fill up the disk + # This is a simple check to ensure the extraction was controlled + assert File.exists?(@tmp_dir) + + {:error, _} -> + # If it returns an error, that's also acceptable + # The important thing is that it doesn't crash or hang + assert true + end + end + + test "handles deeply nested directory structures" do + # Create a zip with deeply nested directories + deep_nest_path = create_deeply_nested_zip() + + # The SafeZip module should handle this gracefully + result = SafeZip.unzip_file(deep_nest_path, @tmp_dir) + + case result do + {:ok, files} -> + # If it successfully extracts, verify the files were extracted + assert is_list(files) + assert length(files) > 0 + + {:error, _} -> + # If it returns an error, that's also acceptable + # The important thing is that it doesn't crash or hang + assert true + end + end + end + + # Helper functions to create test fixtures + + # Creates a zip file with a path traversal attempt + defp create_malicious_zip_with_path_traversal do + malicious_zip_path = Path.join(@tmp_dir, "path_traversal.zip") + + # Create a file to include in the zip + test_file_path = Path.join(@tmp_dir, "test_file.txt") + File.write!(test_file_path, "malicious content") + + # Use Erlang's zip module directly to create a zip with path traversal + {:ok, charlist_path} = + :zip.create( + String.to_charlist(malicious_zip_path), + [{String.to_charlist("../traversal_attempt.txt"), File.read!(test_file_path)}] + ) + + to_string(charlist_path) + end + + # Creates a zip file with directory entries + defp create_zip_with_directory do + zip_path = Path.join(@tmp_dir, "with_directory.zip") + + # Create files to include in the zip + root_file_path = Path.join(@tmp_dir, "root_file.txt") + File.write!(root_file_path, "root file content") + + # Create a directory and a file in it + dir_path = Path.join(@tmp_dir, "file_in_dir") + Pleroma.Backports.mkdir_p!(dir_path) + + file_in_dir_path = Path.join(dir_path, "test_file.txt") + File.write!(file_in_dir_path, "file in directory content") + + # Use Erlang's zip module to create a zip with directory structure + {:ok, charlist_path} = + :zip.create( + String.to_charlist(zip_path), + [ + {String.to_charlist("root_file.txt"), File.read!(root_file_path)}, + {String.to_charlist("file_in_dir/test_file.txt"), File.read!(file_in_dir_path)} + ] + ) + + to_string(charlist_path) + end + + # Creates a zip bomb (a zip with many small files) + defp create_zip_bomb do + zip_path = Path.join(@tmp_dir, "zip_bomb.zip") + + # Create a small file to duplicate many times + small_file_path = Path.join(@tmp_dir, "small_file.txt") + File.write!(small_file_path, String.duplicate("A", 100)) + + # Create a list of many files to include in the zip + file_entries = + for i <- 1..100 do + {String.to_charlist("file_#{i}.txt"), File.read!(small_file_path)} + end + + # Use Erlang's zip module to create a zip with many files + {:ok, charlist_path} = + :zip.create( + String.to_charlist(zip_path), + file_entries + ) + + to_string(charlist_path) + end + + # Creates a zip with deeply nested directories + defp create_deeply_nested_zip do + zip_path = Path.join(@tmp_dir, "deep_nest.zip") + + # Create a file to include in the zip + file_content = "test content" + + # Create a list of deeply nested files + file_entries = + for i <- 1..10 do + nested_path = Enum.reduce(1..i, "nested", fn j, acc -> "#{acc}/level_#{j}" end) + {String.to_charlist("#{nested_path}/file.txt"), file_content} + end + + # Use Erlang's zip module to create a zip with deeply nested directories + {:ok, charlist_path} = + :zip.create( + String.to_charlist(zip_path), + file_entries + ) + + to_string(charlist_path) + end +enddiff --git a/test/pleroma/search/qdrant_search_test.exs b/test/pleroma/search/qdrant_search_test.exs@@ -51,7 +51,7 @@ defmodule Pleroma.Search.QdrantSearchTest do }) Config - |> expect(:get, 3, fn + |> expect(:get, 4, fn [Pleroma.Search, :module], nil -> QdrantSearch @@ -93,7 +93,7 @@ defmodule Pleroma.Search.QdrantSearchTest do }) Config - |> expect(:get, 3, fn + |> expect(:get, 4, fn [Pleroma.Search, :module], nil -> QdrantSearch @@ -158,7 +158,7 @@ defmodule Pleroma.Search.QdrantSearchTest do end) Config - |> expect(:get, 6, fn + |> expect(:get, 7, fn [Pleroma.Search, :module], nil -> QdrantSearchdiff --git a/test/pleroma/upload/filter/analyze_metadata_test.exs b/test/pleroma/upload/filter/analyze_metadata_test.exs@@ -34,6 +34,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T" end + test "it gets dimensions for grayscale images" do + upload = %Pleroma.Upload{ + name: "break_analyze.png", + content_type: "image/png", + path: Path.absname("test/fixtures/break_analyze.png"), + tempfile: Path.absname("test/fixtures/break_analyze.png") + } + + {:ok, :filtered, meta} = AnalyzeMetadata.filter(upload) + + assert %{width: 1410, height: 2048} = meta + assert is_nil(meta.blurhash) + end + test "adds the dimensions for videos" do upload = %Pleroma.Upload{ name: "coolvideo.mp4",diff --git a/test/pleroma/upload/filter/anonymize_filename_test.exs b/test/pleroma/upload/filter/anonymize_filename_test.exs@@ -3,8 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true + import Mox + alias Pleroma.StaticStubbedConfigMock, as: ConfigMock alias Pleroma.Upload setup do @@ -19,21 +21,26 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do %{upload_file: upload_file} end - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - test "it replaces filename on pre-defined text", %{upload_file: upload_file} do - clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + ConfigMock + |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert name == "custom-file.png" end test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do - clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") + ConfigMock + |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.{extension}" end) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert name == "custom-file.jpg" end test "it replaces filename on random text", %{upload_file: upload_file} do + ConfigMock + |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> nil end) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert <<_::bytes-size(14)>> <> ".jpg" = name refute name == "an… image.jpg"diff --git a/test/pleroma/upload/filter/dedupe_test.exs b/test/pleroma/upload/filter/dedupe_test.exs@@ -10,6 +10,10 @@ defmodule Pleroma.Upload.Filter.DedupeTest do @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" + test "generates a shard path for a shasum" do + assert "e3/03/97/" <> _path = Dedupe.shard_path(@shasum) + end + test "adds shasum" do File.cp!( "test/fixtures/image.jpg", @@ -23,10 +27,12 @@ defmodule Pleroma.Upload.Filter.DedupeTest do tempfile: Path.absname("test/fixtures/image_tmp.jpg") } + expected_path = Dedupe.shard_path(@shasum <> ".jpg") + assert { :ok, :filtered, - %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} + %Pleroma.Upload{id: @shasum, path: ^expected_path} } = Dedupe.filter(upload) end enddiff --git a/test/pleroma/upload/filter/mogrifun_test.exs b/test/pleroma/upload/filter/mogrifun_test.exs@@ -3,9 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Upload.Filter.MogrifunTest do - use Pleroma.DataCase - import Mock + use Pleroma.DataCase, async: true + import Mox + alias Pleroma.MogrifyMock alias Pleroma.Upload alias Pleroma.Upload.Filter @@ -22,23 +23,12 @@ defmodule Pleroma.Upload.Filter.MogrifunTest do tempfile: Path.absname("test/fixtures/image_tmp.jpg") } - task = - Task.async(fn -> - assert_receive {:apply_filter, {}}, 4_000 - end) + MogrifyMock + |> stub(:open, fn _file -> %{} end) + |> stub(:custom, fn _image, _action -> %{} end) + |> stub(:custom, fn _image, _action, _options -> %{} end) + |> stub(:save, fn _image, [in_place: true] -> :ok end) - with_mocks([ - {Mogrify, [], - [ - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, - custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, - save: fn _f, _o -> :ok end - ]} - ]) do - assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} - end - - Task.await(task) + assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} end enddiff --git a/test/pleroma/upload/filter/mogrify_test.exs b/test/pleroma/upload/filter/mogrify_test.exs@@ -3,13 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Upload.Filter.MogrifyTest do - use Pleroma.DataCase - import Mock + use Pleroma.DataCase, async: true + import Mox + alias Pleroma.MogrifyMock + alias Pleroma.StaticStubbedConfigMock, as: ConfigMock alias Pleroma.Upload.Filter + setup :verify_on_exit! + test "apply mogrify filter" do - clear_config(Filter.Mogrify, args: [{"tint", "40"}]) + ConfigMock + |> stub(:get!, fn [Filter.Mogrify, :args] -> [{"tint", "40"}] end) File.cp!( "test/fixtures/image.jpg", @@ -23,19 +28,11 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do tempfile: Path.absname("test/fixtures/image_tmp.jpg") } - task = - Task.async(fn -> - assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 - end) - - with_mock Mogrify, - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> :ok end, - custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, - save: fn _f, _o -> :ok end do - assert Filter.Mogrify.filter(upload) == {:ok, :filtered} - end + MogrifyMock + |> expect(:open, fn _file -> %{} end) + |> expect(:custom, fn _image, "tint", "40" -> %{} end) + |> expect(:save, fn _image, [in_place: true] -> :ok end) - Task.await(task) + assert Filter.Mogrify.filter(upload) == {:ok, :filtered} end enddiff --git a/test/pleroma/upload/filter_test.exs b/test/pleroma/upload/filter_test.exs@@ -5,12 +5,13 @@ defmodule Pleroma.Upload.FilterTest do use Pleroma.DataCase + import Mox + alias Pleroma.StaticStubbedConfigMock, as: ConfigMock alias Pleroma.Upload.Filter - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - test "applies filters" do - clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + ConfigMock + |> stub(:get, fn [Pleroma.Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end) File.cp!( "test/fixtures/image.jpg",diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs@@ -149,6 +149,9 @@ defmodule Pleroma.UploadTest do test "copies the file to the configured folder with deduping" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + expected_filename = "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" + + expected_path = Pleroma.Upload.Filter.Dedupe.shard_path(expected_filename) file = %Plug.Upload{ content_type: "image/jpeg", @@ -159,8 +162,7 @@ defmodule Pleroma.UploadTest do {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) assert List.first(data["url"])["href"] == - Pleroma.Upload.base_url() <> - "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" + Path.join([Pleroma.Upload.base_url(), expected_path]) end test "copies the file to the configured folder without deduping" dodiff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs@@ -185,13 +185,13 @@ defmodule Pleroma.User.BackupTest do %{"@language" => "und"} ], "bookmarks" => "bookmarks.json", - "followers" => "http://cofe.io/users/cofe/followers", - "following" => "http://cofe.io/users/cofe/following", + "followers" => "followers.json", + "following" => "following.json", "id" => "http://cofe.io/users/cofe", "inbox" => "http://cofe.io/users/cofe/inbox", "likes" => "likes.json", "name" => "Cofe", - "outbox" => "http://cofe.io/users/cofe/outbox", + "outbox" => "outbox.json", "preferredUsername" => "cofe", "publicKey" => %{ "id" => "http://cofe.io/users/cofe#main-key",diff --git a/test/pleroma/user_relationship_test.exs b/test/pleroma/user_relationship_test.exs@@ -3,11 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.UserRelationshipTest do + alias Pleroma.DateTimeMock alias Pleroma.UserRelationship - use Pleroma.DataCase, async: false + use Pleroma.DataCase, async: true - import Mock + import Mox import Pleroma.Factory describe "*_exists?/2" do @@ -52,6 +53,9 @@ defmodule Pleroma.UserRelationshipTest do end test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do + DateTimeMock + |> stub_with(Pleroma.DateTime.Impl) + for relationship_type <- [ :block, :mute, @@ -80,13 +84,15 @@ defmodule Pleroma.UserRelationshipTest do end test "if record already exists, returns it", %{users: [user1, user2]} do - user_block = - with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do - {:ok, %{inserted_at: ~N[2017-03-17 17:09:58]}} = - UserRelationship.create_block(user1, user2) - end + fixed_datetime = ~N[2017-03-17 17:09:58] + + Pleroma.DateTimeMock + |> expect(:utc_now, 2, fn -> fixed_datetime end) + + {:ok, %{inserted_at: ^fixed_datetime}} = UserRelationship.create_block(user1, user2) - assert user_block == UserRelationship.create_block(user1, user2) + # Test the idempotency without caring about the exact time + assert {:ok, _} = UserRelationship.create_block(user1, user2) end enddiff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs@@ -20,7 +20,7 @@ defmodule Pleroma.UserTest do import Swoosh.TestAssertions setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok end @@ -2405,8 +2405,8 @@ defmodule Pleroma.UserTest do other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "https://remote.org/users/masto_closed/followers", + following_address: "https://remote.org/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2426,8 +2426,8 @@ defmodule Pleroma.UserTest do other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "https://remote.org/users/masto_closed/followers", + following_address: "https://remote.org/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2447,8 +2447,8 @@ defmodule Pleroma.UserTest do other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "https://remote.org/users/masto_closed/followers", + following_address: "https://remote.org/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2669,8 +2669,12 @@ defmodule Pleroma.UserTest do assert {:ok, user} = User.update_last_active_at(user) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] last_active_at = NaiveDateTime.utc_now() @@ -2682,10 +2686,15 @@ defmodule Pleroma.UserTest do |> cast(%{last_active_at: last_active_at}, [:last_active_at]) |> User.update_and_set_cache() - assert user.last_active_at == last_active_at + assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq + assert {:ok, user} = User.update_last_active_at(user) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] end test "active_user_count/1" do @@ -2783,6 +2792,15 @@ defmodule Pleroma.UserTest do assert user_updated.also_known_as |> length() == 1 assert user2.ap_id in user_updated.also_known_as end + + test "should tolerate non-http(s) aliases" do + user = + insert(:user, %{ + also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"] + }) + + assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as + end end describe "alias_users/1" do @@ -2919,4 +2937,74 @@ defmodule Pleroma.UserTest do assert [%{"verified_at" => ^verified_at}] = user.fields end + + describe "follow_hashtag/2" do + test "should follow a hashtag" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.followed_hashtags |> Enum.count() == 1 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + + test "should not follow a hashtag twice" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.followed_hashtags |> Enum.count() == 1 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + + test "can follow multiple hashtags" do + user = insert(:user) + hashtag = insert(:hashtag) + other_hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {:ok, _} = user |> User.follow_hashtag(other_hashtag) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.followed_hashtags |> Enum.count() == 2 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + end + + describe "unfollow_hashtag/2" do + test "should unfollow a hashtag" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {:ok, _} = user |> User.unfollow_hashtag(hashtag) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.followed_hashtags |> Enum.count() == 0 + end + + test "should not error when trying to unfollow a hashtag twice" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {:ok, _} = user |> User.unfollow_hashtag(hashtag) + assert {:ok, _} = user |> User.unfollow_hashtag(hashtag) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.followed_hashtags |> Enum.count() == 0 + end + end enddiff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Activity alias Pleroma.Delivery - alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Tests.ObanHelpers alias Pleroma.User @@ -26,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do require Pleroma.Constants setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok end @@ -601,23 +600,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end - test "it clears `unreachable` federation status of the sender", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() - - sender_url = data["actor"] - Instances.set_consistently_unreachable(sender_url) - refute Instances.reachable?(sender_url) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_url) - end - test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() @@ -941,23 +923,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end - test "it rejects an invalid incoming activity", %{conn: conn, data: data} do - user = insert(:user, is_active: false) - - data = - data - |> Map.put("bcc", [user.ap_id]) - |> Kernel.put_in(["object", "bcc"], [user.ap_id]) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "Invalid request." == json_response(conn, 400) - end - test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do user = insert(:user) @@ -1108,24 +1073,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert response(conn, 200) =~ note_object.data["content"] end - test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "bcc", [user.ap_id]) - - sender_host = URI.parse(data["actor"]).host - Instances.set_consistently_unreachable(sender_host) - refute Instances.reachable?(sender_host) - - 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) - assert Instances.reachable?(sender_host) - end - test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) @@ -1320,9 +1267,79 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do html_body: ~r/#{note.data["object"]}/i ) end + + test "it accepts an incoming Block", %{conn: conn, data: data} do + user = insert(:user) + + data = + data + |> Map.put("type", "Block") + |> Map.put("to", [user.ap_id]) + |> Map.put("cc", []) + |> Map.put("object", user.ap_id) + + 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 returns an error when receiving an activity sent to a deactivated user", %{ + conn: conn, + data: data + } do + user = insert(:user) + {:ok, _} = User.set_activation(user, false) + + data = + data + |> Map.put("bcc", [user.ap_id]) + |> Kernel.put_in(["object", "bcc"], [user.ap_id]) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "User deactivated" == json_response(conn, 404) + end + + test "it returns an error when receiving an activity sent from a deactivated user", %{ + conn: conn, + data: data + } do + sender = insert(:user) + user = insert(:user) + {:ok, _} = User.set_activation(sender, false) + + data = + data + |> Map.put("bcc", [user.ap_id]) + |> Map.put("actor", sender.ap_id) + |> Kernel.put_in(["object", "bcc"], [user.ap_id]) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "Sender deactivated" == json_response(conn, 404) + end end describe "GET /users/:nickname/outbox" do + setup do + Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config) + :ok + end + test "it paginates correctly", %{conn: conn} do user = insert(:user) conn = assign(conn, :user, user) @@ -1411,6 +1428,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert %{"orderedItems" => []} = resp end + test "it does not return a local note activity when C2S API is disabled", %{conn: conn} do + clear_config([:activitypub, :client_api_enabled], false) + user = insert(:user) + reader = insert(:user) + {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) + + resp = + conn + |> assign(:user, reader) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + |> json_response(200) + + assert %{"orderedItems" => []} = resp + end + test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false) @@ -1462,6 +1495,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert [answer_outbox] = outbox_get["orderedItems"] assert answer_outbox["id"] == activity.data["id"] end + + test "it works with authorized fetch forced when authenticated" do + clear_config([:activitypub, :authorized_fetch_mode], true) + + user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" + + conn = + build_conn() + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint) + + assert json_response(conn, 200) + end + + test "it fails with authorized fetch forced when unauthenticated", %{conn: conn} do + clear_config([:activitypub, :authorized_fetch_mode], true) + + user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint) + + assert response(conn, 401) + end end describe "POST /users/:nickname/outbox (C2S)" do @@ -1623,6 +1685,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert json_response(conn, 403) end + test "it rejects update activity of object from other actor", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity, fetch: false) + user = insert(:user) + + data = %{ + type: "Update", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 400) + assert note_object == Object.normalize(note_activity, fetch: false) + end + test "it increases like count when receiving a like action", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false) @@ -2110,6 +2194,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) |> json_response(403) end + + test "they don't work when C2S API is disabled", %{conn: conn} do + clear_config([:activitypub, :client_api_enabled], false) + + user = insert(:user) + + assert conn + |> assign(:user, user) + |> get("/api/ap/whoami") + |> response(403) + + desc = "Description of the image" + + image = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + assert conn + |> assign(:user, user) + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> response(403) + end end test "pinned collection", %{conn: conn} dodiff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs@@ -232,12 +232,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert user.avatar == %{ "type" => "Image", - "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}] + "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}], + "name" => "profile picture" } assert user.banner == %{ "type" => "Image", - "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}] + "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}], + "name" => "profile picture" } end @@ -432,6 +434,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert user.birthday == ~D[2001-02-12] end + + test "fetches avatar description" do + user_id = "https://example.com/users/marcin" + + user_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "marcin") + |> Jason.decode!() + |> Map.delete("featured") + |> Map.update("icon", %{}, fn image -> Map.put(image, "name", "image description") end) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^user_id + } -> + %Tesla.Env{ + status: 200, + body: user_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + + assert user.avatar["name"] == "image description" + end end test "it fetches the appropriate tag-restricted posts" do @@ -795,7 +826,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert object.data["repliesCount"] == 2 end - test "increates quotes count", %{user: user} do + test "increases quotes count", %{user: user} do user2 = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) @@ -836,6 +867,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end + describe "fetch activities for followed hashtags" do + test "it should return public activities that reference a given hashtag" do + hashtag = insert(:hashtag, name: "tenshi") + user = insert(:user) + other_user = insert(:user) + + {:ok, normally_visible} = + CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"}) + + {:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"}) + {:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"}) + {:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"}) + + activities = + ActivityPub.fetch_activities([other_user.follower_address], %{ + followed_hashtags: [hashtag.id] + }) + + assert length(activities) == 3 + normal_id = normally_visible.id + public_id = public.id + unlisted_id = unlisted.id + assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities + end + end + describe "fetch activities in context" do test "retrieves activities that have a given context" do {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) @@ -1727,8 +1785,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" + follower_address: "https://remote.org/users/fuser2/followers", + following_address: "https://remote.org/users/fuser2/following" ) {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1739,7 +1797,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do test "detects hidden followers" do mock(fn env -> case env.url do - "http://localhost:4001/users/masto_closed/followers?page=1" -> + "https://remote.org/users/masto_closed/followers?page=1" -> %Tesla.Env{status: 403, body: ""} _ -> @@ -1750,8 +1808,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "https://remote.org/users/masto_closed/followers", + following_address: "https://remote.org/users/masto_closed/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1762,7 +1820,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do test "detects hidden follows" do mock(fn env -> case env.url do - "http://localhost:4001/users/masto_closed/following?page=1" -> + "https://remote.org/users/masto_closed/following?page=1" -> %Tesla.Env{status: 403, body: ""} _ -> @@ -1773,8 +1831,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "https://remote.org/users/masto_closed/followers", + following_address: "https://remote.org/users/masto_closed/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1786,8 +1844,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do user = insert(:user, local: false, - follower_address: "http://localhost:8080/followers/fuser3", - following_address: "http://localhost:8080/following/fuser3" + follower_address: "https://remote.org/followers/fuser3", + following_address: "https://remote.org/following/fuser3" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1800,28 +1858,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do 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" -> + "https://remote.org/users/masto_hidden_counters/following" -> json( %{ "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + "id" => "https://remote.org/users/masto_hidden_counters/followers" }, headers: HttpRequestMock.activitypub_object_headers() ) - "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> + "https://remote.org/users/masto_hidden_counters/following?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_hidden_counters/followers" -> + "https://remote.org/users/masto_hidden_counters/followers" -> json( %{ "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/following" + "id" => "https://remote.org/users/masto_hidden_counters/following" }, headers: HttpRequestMock.activitypub_object_headers() ) - "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> + "https://remote.org/users/masto_hidden_counters/followers?page=1" -> %Tesla.Env{status: 403, body: ""} end end) @@ -1829,8 +1887,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do 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" + follower_address: "https://remote.org/users/masto_hidden_counters/followers", + following_address: "https://remote.org/users/masto_hidden_counters/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)diff --git a/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs b/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs@@ -1,117 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.FODirectReplyTest do - use Pleroma.DataCase - import Pleroma.Factory - - require Pleroma.Constants - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.MRF.FODirectReply - alias Pleroma.Web.CommonAPI - - test "replying to followers-only/private is changed to direct" do - batman = insert(:user, nickname: "batman") - robin = insert(:user, nickname: "robin") - - {:ok, post} = - CommonAPI.post(batman, %{ - status: "Has anyone seen Selina Kyle's latest selfies?", - visibility: "private" - }) - - reply = %{ - "type" => "Create", - "actor" => robin.ap_id, - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "object" => %{ - "type" => "Note", - "actor" => robin.ap_id, - "content" => "@batman 🤤 ❤️ 🐈⬛", - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "inReplyTo" => Object.normalize(post).data["id"] - } - } - - expected_to = [batman.ap_id] - expected_cc = [] - - assert {:ok, filtered} = FODirectReply.filter(reply) - - assert expected_to == filtered["to"] - assert expected_cc == filtered["cc"] - assert expected_to == filtered["object"]["to"] - assert expected_cc == filtered["object"]["cc"] - end - - test "replies to unlisted posts are unmodified" do - batman = insert(:user, nickname: "batman") - robin = insert(:user, nickname: "robin") - - {:ok, post} = - CommonAPI.post(batman, %{ - status: "Has anyone seen Selina Kyle's latest selfies?", - visibility: "unlisted" - }) - - reply = %{ - "type" => "Create", - "actor" => robin.ap_id, - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "object" => %{ - "type" => "Note", - "actor" => robin.ap_id, - "content" => "@batman 🤤 ❤️ 🐈<200d>⬛", - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "inReplyTo" => Object.normalize(post).data["id"] - } - } - - assert {:ok, filtered} = FODirectReply.filter(reply) - - assert match?(^filtered, reply) - end - - test "replies to public posts are unmodified" do - batman = insert(:user, nickname: "batman") - robin = insert(:user, nickname: "robin") - - {:ok, post} = - CommonAPI.post(batman, %{status: "Has anyone seen Selina Kyle's latest selfies?"}) - - reply = %{ - "type" => "Create", - "actor" => robin.ap_id, - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "object" => %{ - "type" => "Note", - "actor" => robin.ap_id, - "content" => "@batman 🤤 ❤️ 🐈<200d>⬛", - "to" => [batman.ap_id, robin.follower_address], - "cc" => [], - "inReplyTo" => Object.normalize(post).data["id"] - } - } - - assert {:ok, filtered} = FODirectReply.filter(reply) - - assert match?(^filtered, reply) - end - - test "non-reply posts are unmodified" do - batman = insert(:user, nickname: "batman") - - {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"}) - - assert {:ok, filtered} = FODirectReply.filter(post) - - assert match?(^filtered, post) - end -enddiff --git a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs@@ -39,15 +39,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do } } - expected_to = [batman.ap_id, robin.follower_address] - expected_cc = [Pleroma.Constants.as_public()] - assert {:ok, filtered} = QuietReply.filter(reply) - assert expected_to == filtered["to"] - assert expected_cc == filtered["cc"] - assert expected_to == filtered["object"]["to"] - assert expected_cc == filtered["object"]["cc"] + assert batman.ap_id in filtered["to"] + assert batman.ap_id in filtered["object"]["to"] + assert robin.follower_address in filtered["to"] + assert robin.follower_address in filtered["object"]["to"] + assert Pleroma.Constants.as_public() in filtered["cc"] + assert Pleroma.Constants.as_public() in filtered["object"]["cc"] end test "replying to unlisted post is unmodified" dodiff --git a/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs@@ -0,0 +1,155 @@ +defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy + + setup do + clear_config([:mrf_remote_report, :reject_all], false) + end + + test "doesn't impact local report" do + clear_config([:mrf_remote_report, :reject_anonymous], true) + clear_config([:mrf_remote_report, :reject_empty_message], true) + + activity = %{ + "type" => "Flag", + "actor" => "http://localhost:4001/actor", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "rejects anonymous report if `reject_anonymous: true`" do + clear_config([:mrf_remote_report, :reject_anonymous], true) + clear_config([:mrf_remote_report, :reject_empty_message], true) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/actor", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:reject, _} = RemoteReportPolicy.filter(activity) + end + + test "preserves anonymous report if `reject_anonymous: false`" do + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/actor", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "rejects report on third party if `reject_third_party: true`" do + clear_config([:mrf_remote_report, :reject_third_party], true) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:reject, _} = RemoteReportPolicy.filter(activity) + end + + test "preserves report on first party if `reject_third_party: true`" do + clear_config([:mrf_remote_report, :reject_third_party], true) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["http://localhost:4001/actor"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "preserves report on third party if `reject_third_party: false`" do + clear_config([:mrf_remote_report, :reject_third_party], false) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "rejects empty message report if `reject_empty_message: true`" do + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], true) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:reject, _} = RemoteReportPolicy.filter(activity) + end + + test "rejects empty message report (\"\") if `reject_empty_message: true`" do + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], true) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["https://mastodon.online/users/Gargron"], + "content" => "" + } + + assert {:reject, _} = RemoteReportPolicy.filter(activity) + end + + test "preserves empty message report if `reject_empty_message: false`" do + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "preserves anonymous, empty message report with all settings disabled" do + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/actor", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:ok, _} = RemoteReportPolicy.filter(activity) + end + + test "reject remote report if `reject_all: true`" do + clear_config([:mrf_remote_report, :reject_all], true) + clear_config([:mrf_remote_report, :reject_anonymous], false) + clear_config([:mrf_remote_report, :reject_empty_message], false) + + activity = %{ + "type" => "Flag", + "actor" => "https://mastodon.social/users/Gargron", + "content" => "Transphobia", + "object" => ["https://mastodon.online/users/Gargron"] + } + + assert {:reject, _} = RemoteReportPolicy.filter(activity) + end +enddiff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs@@ -252,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do remote_message = build_remote_message() assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + assert SimplePolicy.id_filter(remote_message["actor"]) end test "activity has a matching host" do @@ -260,6 +261,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do remote_message = build_remote_message() assert {:reject, _} = SimplePolicy.filter(remote_message) + refute SimplePolicy.id_filter(remote_message["actor"]) end test "activity matches with wildcard domain" do @@ -268,6 +270,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do remote_message = build_remote_message() assert {:reject, _} = SimplePolicy.filter(remote_message) + refute SimplePolicy.id_filter(remote_message["actor"]) end test "actor has a matching host" do @@ -276,6 +279,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do remote_user = build_remote_user() assert {:reject, _} = SimplePolicy.filter(remote_user) + refute SimplePolicy.id_filter(remote_user["id"]) end test "reject Announce when object would be rejected" do @@ -288,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do } assert {:reject, _} = SimplePolicy.filter(announce) + # Note: Non-Applicable for id_filter/1 end test "reject by URI object" do @@ -300,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do } assert {:reject, _} = SimplePolicy.filter(announce) + # Note: Non-Applicable for id_filter/1 end end @@ -370,6 +376,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + assert SimplePolicy.id_filter(local_message["actor"]) + assert SimplePolicy.id_filter(remote_message["actor"]) end test "is not empty but activity doesn't have a matching host" do @@ -380,6 +388,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} assert {:reject, _} = SimplePolicy.filter(remote_message) + assert SimplePolicy.id_filter(local_message["actor"]) + refute SimplePolicy.id_filter(remote_message["actor"]) end test "activity has a matching host" do @@ -390,6 +400,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + assert SimplePolicy.id_filter(local_message["actor"]) + assert SimplePolicy.id_filter(remote_message["actor"]) end test "activity matches with wildcard domain" do @@ -400,6 +412,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + assert SimplePolicy.id_filter(local_message["actor"]) + assert SimplePolicy.id_filter(remote_message["actor"]) end test "actor has a matching host" do @@ -408,6 +422,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do remote_user = build_remote_user() assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + assert SimplePolicy.id_filter(remote_user["id"]) end enddiff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do assert File.exists?(fullpath) end - test "rejects invalid shortcodes", %{path: path} do + test "rejects invalid shortcodes with slashes", %{path: path} do message = %{ "type" => "Create", "object" => %{ @@ -113,6 +113,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do refute File.exists?(fullpath) end + test "rejects invalid shortcodes with dots", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fired.fox", "https://example.org/emoji/firedfox"}], + "actor" => "https://example.org/users/admin" + } + } + + fullpath = Path.join(path, "fired.fox.png") + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "fired.fox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + refute "fired.fox" in installed() + refute File.exists?(fullpath) + end + + test "rejects invalid shortcodes with special characters", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fired:fox", "https://example.org/emoji/firedfox"}], + "actor" => "https://example.org/users/admin" + } + } + + fullpath = Path.join(path, "fired:fox.png") + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "fired:fox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + refute "fired:fox" in installed() + refute File.exists?(fullpath) + end + test "reject regex shortcode", %{message: message} do refute "firedfox" in installed() @@ -171,5 +223,74 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do refute "firedfox" in installed() end + test "accepts valid alphanum shortcodes", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fire1fox", "https://example.org/emoji/fire1fox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire1fox.png"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "fire1fox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "fire1fox" in installed() + end + + test "accepts valid shortcodes with underscores", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fire_fox", "https://example.org/emoji/fire_fox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire_fox.png"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "fire_fox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "fire_fox" in installed() + end + + test "accepts valid shortcodes with hyphens", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fire-fox", "https://example.org/emoji/fire-fox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire-fox.png"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "fire-fox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "fire-fox" in installed() + end + defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end) enddiff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs@@ -5,12 +5,33 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do use Pleroma.DataCase, async: true + alias Pleroma.Language.LanguageDetectorMock + alias Pleroma.StaticStubbedConfigMock alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.Utils + import Mox import Pleroma.Factory + # Setup for all tests + setup do + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + # Stub the LanguageDetectorMock with default implementations + LanguageDetectorMock + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + |> stub(:detect, fn _text -> nil end) + + :ok + end + describe "Notes" do setup do user = insert(:user) @@ -128,6 +149,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "a Note with validated likes collection validates" do + insert(:user, ap_id: "https://pol.social/users/mkljczk") + + %{"object" => note} = + "test/fixtures/mastodon-update-with-likes.json" + |> File.read!() + |> Jason.decode!() + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + test "Fedibird quote post" do insert(:user, ap_id: "https://fedibird.com/users/noellabo") @@ -176,4 +208,102 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest name: "RE: https://server.example/objects/123" } end + + describe "Note language" do + test "it detects language from JSON-LD context" do + user = insert(:user) + + note_activity = %{ + "@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}], + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "Szczęść Boże", + "attributedTo" => user.ap_id + }, + "actor" => user.ap_id + } + + {:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, []) + + assert meta[:object_data]["language"] == "pl" + end + + test "it detects language from contentMap" do + user = insert(:user) + + note = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "Szczęść Boże", + "contentMap" => %{ + "de" => "Gott segne", + "pl" => "Szczęść Boże" + }, + "attributedTo" => user.ap_id + } + + {:ok, object} = ArticleNotePageValidator.cast_and_apply(note) + + assert object.language == "pl" + end + + test "it doesn't call LanguageDetector when language is specified" do + # Set up expectation that detect should not be called + LanguageDetectorMock + |> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end) + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + user = insert(:user) + + note = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "a post in English", + "contentMap" => %{ + "en" => "a post in English" + }, + "attributedTo" => user.ap_id + } + + ArticleNotePageValidator.cast_and_apply(note) + end + + test "it adds contentMap if language is specified" do + user = insert(:user) + + note = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "тест", + "language" => "uk", + "attributedTo" => user.ap_id + } + + {:ok, object} = ArticleNotePageValidator.cast_and_apply(note) + + assert object.contentMap == %{ + "uk" => "тест" + } + end + end enddiff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs@@ -13,6 +13,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do import Pleroma.Factory describe "attachments" do + test "works with apng" do + attachment = + %{ + "mediaType" => "image/apng", + "name" => "", + "type" => "Document", + "url" => + "https://media.misskeyusercontent.com/io/2859c26e-cd43-4550-848b-b6243bc3fe28.apng" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/apng" + end + test "fails without url" do attachment = %{ "mediaType" => "",diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs@@ -6,13 +6,11 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do use Oban.Testing, repo: Pleroma.Repo 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.Tests.ObanHelpers alias Pleroma.Web.ActivityPub.Publisher @@ -167,115 +165,6 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do }) |> Publisher.publish_one() 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" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string() - }) - |> Publisher.publish_one() - - 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" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: nil - }) - |> Publisher.publish_one() - - 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" - activity = insert(:note_activity) - - assert {:cancel, _} = - Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id}) - |> Publisher.publish_one() - - 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" - activity = insert(:note_activity) - - assert capture_log(fn -> - assert {:error, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id - }) - |> Publisher.publish_one() - 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" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id}) - |> Publisher.publish_one() - - 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" - activity = insert(:note_activity) - - assert capture_log(fn -> - assert {:error, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string() - }) - |> Publisher.publish_one() - end) =~ "connrefused" - - refute called(Instances.set_unreachable(inbox)) - end end describe "publish/2" do @@ -520,4 +409,105 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end + + test "unlisted activities retain public address in cc" do + user = insert(:user) + + # simulate unlistd activity by only having + # public address in cc + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert @as_public in decoded["cc"] + + # maybe we also have another inbox in cc + # during Publishing + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://remote.instance/users/someone_else/inbox"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"] + end + + test "public address in cc parameter is preserved" do + user = insert(:user) + + cc_with_public = [@as_public, "https://example.org/users/other"] + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => cc_with_public, + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: cc_with_public + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert cc_with_public == decoded["cc"] + end + + test "cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => ["https://example.com/specific/user"], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == ["https://example.com/specific/user"] + end enddiff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do use Pleroma.DataCase, async: true alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI @@ -75,4 +76,107 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do assert activity_data["object"] == activity.data["object"] assert activity_data["content"] == "⭐" end + + test "it works for misskey likes with custom emoji" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == ":blobwtfnotlikethis:" + + assert [["blobwtfnotlikethis", _, _]] = + Object.get_by_ap_id(activity.data["object"]) + |> Object.get_emoji_reactions() + end + + test "it works for mitra likes with custom emoji" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mitra-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == ":ablobcatheartsqueeze:" + + assert [["ablobcatheartsqueeze", _, _]] = + Object.get_by_ap_id(activity.data["object"]) + |> Object.get_emoji_reactions() + end + + test "it works for likes with wrong content" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mitra-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("content", 1) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + assert activity.data["type"] == "Like" + end + + test "it changes incoming dislikes into emoji reactions" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/friendica-dislike.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "EmojiReact" + assert data["content"] == "👎" + assert data["id"] == "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + assert data["object"] == activity.data["object"] + + data = + File.read!("test/fixtures/friendica-dislike-undo.json") + |> Jason.decode!() + |> put_in(["object", "object"], activity.data["object"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "Undo" + + assert data["object"] == + "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + end enddiff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs@@ -219,6 +219,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>" end + test "it only uses contentMap if content is not present" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "Hi", + "contentMap" => %{ + "de" => "Hallo", + "uk" => "Привіт" + }, + "inReplyTo" => nil, + "attributedTo" => user.ap_id + }, + "actor" => user.ap_id + } + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(message) + object = Object.normalize(data["object"], fetch: false) + + assert object.data["content"] == "Hi" + end + test "it works for incoming notices with a nil contentMap (firefish)" do data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json")diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs@@ -156,6 +156,246 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do # It fetched the quoted post assert Object.normalize("https://misskey.io/notes/8vs6wxufd0") end + + test "doesn't allow remote edits to fake local likes" do + # as a spot check for no internal fields getting injected + now = DateTime.utc_now() + pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3))) + edit_date = DateTime.to_iso8601(now) + + local_user = insert(:user) + + create_data = %{ + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "http://mastodon.example.org/users/admin/statuses/2619539638", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "published" => pub_date, + "content" => "miaow", + "likes" => [local_user.ap_id] + } + } + + update_data = + create_data + |> Map.put("type", "Update") + |> Map.put("id", create_data["object"]["id"] <> "/update/1") + |> put_in(["object", "content"], "miaow :3") + |> put_in(["object", "updated"], edit_date) + |> put_in(["object", "formerRepresentations"], %{ + "type" => "OrderedCollection", + "totalItems" => 1, + "orderedItems" => [create_data["object"]] + }) + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["content"] == "miaow" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"]) + assert object.data["content"] == "miaow :3" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + end + + test "strips internal fields from history items in edited notes" do + now = DateTime.utc_now() + pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3))) + edit_date = DateTime.to_iso8601(now) + + local_user = insert(:user) + + create_data = %{ + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "http://mastodon.example.org/users/admin/statuses/2619539638", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "published" => pub_date, + "content" => "miaow", + "likes" => [], + "like_count" => 0 + } + } + + update_data = + create_data + |> Map.put("type", "Update") + |> Map.put("id", create_data["object"]["id"] <> "/update/1") + |> put_in(["object", "content"], "miaow :3") + |> put_in(["object", "updated"], edit_date) + |> put_in(["object", "formerRepresentations"], %{ + "type" => "OrderedCollection", + "totalItems" => 1, + "orderedItems" => [ + Map.merge(create_data["object"], %{ + "likes" => [local_user.ap_id], + "like_count" => 1, + "pleroma" => %{"internal_field" => "should_be_stripped"} + }) + ] + }) + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["content"] == "miaow" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"]) + assert object.data["content"] == "miaow :3" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + + # Check that internal fields are stripped from history items + history_item = List.first(object.data["formerRepresentations"]["orderedItems"]) + assert history_item["likes"] == [] + assert history_item["like_count"] == 0 + refute Map.has_key?(history_item, "pleroma") + end + + test "doesn't trip over remote likes in notes" do + now = DateTime.utc_now() + pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3))) + edit_date = DateTime.to_iso8601(now) + + create_data = %{ + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/statuses/3409297097/activity", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "http://mastodon.example.org/users/admin/statuses/3409297097", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "published" => pub_date, + "content" => "miaow", + "likes" => %{ + "id" => "http://mastodon.example.org/users/admin/statuses/3409297097/likes", + "totalItems" => 0, + "type" => "Collection" + } + } + } + + update_data = + create_data + |> Map.put("type", "Update") + |> Map.put("id", create_data["object"]["id"] <> "/update/1") + |> put_in(["object", "content"], "miaow :3") + |> put_in(["object", "updated"], edit_date) + |> put_in(["object", "likes", "totalItems"], 666) + |> put_in(["object", "formerRepresentations"], %{ + "type" => "OrderedCollection", + "totalItems" => 1, + "orderedItems" => [create_data["object"]] + }) + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["content"] == "miaow" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"]) + assert object.data["content"] == "miaow :3" + assert object.data["likes"] == [] + # in the future this should retain remote likes, but for now: + assert object.data["like_count"] == 0 + end + + test "doesn't trip over remote likes in polls" do + now = DateTime.utc_now() + pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3))) + edit_date = DateTime.to_iso8601(now) + + create_data = %{ + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/statuses/2471790073/activity", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Question", + "id" => "http://mastodon.example.org/users/admin/statuses/2471790073", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "published" => pub_date, + "content" => "vote!", + "anyOf" => [ + %{ + "type" => "Note", + "name" => "a", + "replies" => %{ + "type" => "Collection", + "totalItems" => 3 + } + }, + %{ + "type" => "Note", + "name" => "b", + "replies" => %{ + "type" => "Collection", + "totalItems" => 1 + } + } + ], + "likes" => %{ + "id" => "http://mastodon.example.org/users/admin/statuses/2471790073/likes", + "totalItems" => 0, + "type" => "Collection" + } + } + } + + update_data = + create_data + |> Map.put("type", "Update") + |> Map.put("id", create_data["object"]["id"] <> "/update/1") + |> put_in(["object", "content"], "vote now!") + |> put_in(["object", "updated"], edit_date) + |> put_in(["object", "likes", "totalItems"], 666) + |> put_in(["object", "formerRepresentations"], %{ + "type" => "OrderedCollection", + "totalItems" => 1, + "orderedItems" => [create_data["object"]] + }) + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["content"] == "vote!" + assert object.data["likes"] == [] + assert object.data["like_count"] == 0 + + {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data) + %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"]) + assert object.data["content"] == "vote now!" + assert object.data["likes"] == [] + # in the future this should retain remote likes, but for now: + assert object.data["like_count"] == 0 + end end describe "prepare outgoing" do @@ -384,6 +624,24 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert modified["object"]["quoteUrl"] == quote_id assert modified["object"]["quoteUri"] == quote_id end + + test "it adds language of the object to its json-ld context" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data) + + assert [_, _, %{"@language" => "pl"}] = modified["@context"] + end + + test "it adds language of the object to Create activity json-ld context" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert [_, _, %{"@language" => "pl"}] = modified["@context"] + end end describe "actor rewriting" do @@ -621,5 +879,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do processed = Transmogrifier.prepare_object(original) assert processed["formerRepresentations"] == original["formerRepresentations"] end + + test "it uses contentMap to specify post language" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) + object = Transmogrifier.prepare_object(activity.object.data) + + assert %{"contentMap" => %{"pl" => "Cześć"}} = object + end end enddiff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs@@ -173,16 +173,30 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end - test "make_json_ld_header/0" do - assert Utils.make_json_ld_header() == %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{ - "@language" => "und" - } - ] - } + describe "make_json_ld_header/1" do + test "makes jsonld header" do + assert Utils.make_json_ld_header() == %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{ + "@language" => "und" + } + ] + } + end + + test "includes language if specified" do + assert Utils.make_json_ld_header(%{"language" => "pl"}) == %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{ + "@language" => "pl" + } + ] + } + end end describe "get_existing_votes" dodiff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs@@ -68,6 +68,23 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do result = UserView.render("user.json", %{user: user}) assert result["icon"]["url"] == "https://someurl" assert result["image"]["url"] == "https://somebanner" + + refute result["icon"]["name"] + refute result["image"]["name"] + end + + test "Avatar has a description if the user set one" do + user = + insert(:user, + avatar: %{ + "url" => [%{"href" => "https://someurl"}], + "name" => "a drawing of pleroma-tan using pleroma groups" + } + ) + + result = UserView.render("user.json", %{user: user}) + + assert result["icon"]["name"] == "a drawing of pleroma-tan using pleroma groups" end test "renders an invisible user with the invisible property set to true" dodiff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs@@ -1211,8 +1211,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do end test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do - clear_config(Pleroma.Upload.Filter.Mogrify) - assert conn |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/config", %{ @@ -1240,7 +1238,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do "need_reboot" => false } - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] + config = Config.get(Pleroma.Upload.Filter.Mogrify) + assert {:args, ["auto-orient", "strip"]} in config assert conn |> put_req_header("content-type", "application/json") @@ -1289,9 +1288,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do "need_reboot" => false } - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ - args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] - ] + config = Config.get(Pleroma.Upload.Filter.Mogrify) + + assert {:args, ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]} in config end test "enables the welcome messages", %{conn: conn} dodiff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs@@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do setup do clear_config([:instance, :static_dir], @dir) - File.mkdir_p!(Pleroma.Frontend.dir()) + Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir()) on_exit(fn -> File.rm_rf(@dir)diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs@@ -8,8 +8,6 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do import Pleroma.Factory - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers alias Pleroma.Web.CommonAPI setup_all do @@ -69,19 +67,19 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do test "DELETE /instances/:instance", %{conn: conn} do clear_config([:instance, :admin_privileges], [:instances_delete]) - user = insert(:user, nickname: "lain@lain.com") - post = insert(:note_activity, user: user) + insert(:user, nickname: "lain@lain.com") response = conn |> delete("/api/pleroma/admin/instances/lain.com") |> json_response(200) - [:ok] = ObanHelpers.perform_all() - assert response == "lain.com" - refute Repo.reload(user).is_active - refute Repo.reload(post) + + assert_enqueued( + worker: Pleroma.Workers.DeleteWorker, + args: %{"op" => "delete_instance", "host" => "lain.com"} + ) clear_config([:instance, :admin_privileges], [])diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs@@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) enddiff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs@@ -20,7 +20,7 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do alias Pleroma.Web.MediaProxy setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs@@ -111,6 +111,17 @@ defmodule Pleroma.Web.CommonAPITest do end end + test "add expiring block", %{blocker: blocker, blocked: blocked} do + {:ok, _} = CommonAPI.block(blocked, blocker, %{expires_in: 60}) + assert User.blocks?(blocker, blocked) + + worker = Pleroma.Workers.MuteExpireWorker + args = %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id} + + assert :ok = perform_job(worker, args) + refute User.blocks?(blocker, blocked) + end + test "it blocks and does not federate if outgoing blocks are disabled", %{ blocker: blocker, blocked: blockeddiff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs@@ -32,7 +32,7 @@ defmodule Pleroma.Web.FallbackTest do resp = get(conn, "/foo") assert html_response(resp, 200) =~ "<title>a cool title</title>" - refute html_response(resp, 200) =~ "initial-results" + assert html_response(resp, 200) =~ "<meta content=\"noindex, noarchive\" name=\"robots\">" end test "GET /*path", %{conn: conn} dodiff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs@@ -126,22 +126,17 @@ defmodule Pleroma.Web.FederatorTest do inbox: inbox2 }) - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(inbox1, dt) - - Instances.set_consistently_unreachable(URI.parse(inbox2).host) + Instances.set_unreachable(URI.parse(inbox2).host) {:ok, _activity} = CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) - expected_dt = NaiveDateTime.to_iso8601(dt) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) assert ObanHelpers.member?( %{ "op" => "publish_one", - "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} + "params" => %{"inbox" => inbox1} }, all_enqueued(worker: PublisherWorker) )diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs@@ -191,4 +191,60 @@ defmodule Pleroma.Web.Feed.TagControllerTest do |> response(404) end end + + describe "restricted for unauthenticated" do + test "returns 404 when local timeline is disabled", %{conn: conn} do + clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: false}) + + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> response(404) + end + + test "returns local posts only when federated timeline is disabled", %{conn: conn} do + clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: true}) + + local_user = insert(:user) + remote_user = insert(:user, local: false) + + local_note = + insert(:note, + user: local_user, + data: %{ + "content" => "local post #PleromaArt", + "summary" => "", + "tag" => ["pleromaart"] + } + ) + + remote_note = + insert(:note, + user: remote_user, + data: %{ + "content" => "remote post #PleromaArt", + "summary" => "", + "tag" => ["pleromaart"] + }, + local: false + ) + + insert(:note_activity, user: local_user, note: local_note) + insert(:note_activity, user: remote_user, note: remote_note, local: false) + + response = + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> response(200) + + xml = parse(response) + + assert xpath(xml, ~x"//channel/title/text()") == ~c"#pleromaart" + + assert xpath(xml, ~x"//channel/item/title/text()"l) == [ + ~c"local post #PleromaArt" + ] + end + end enddiff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs@@ -147,6 +147,15 @@ defmodule Pleroma.Web.Feed.UserControllerTest do assert response(conn, 404) end + test "returns noindex meta for missing user", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "text/html") + |> get("/users/nonexisting") + + assert html_response(conn, 200) =~ "<meta content=\"noindex, noarchive\" name=\"robots\">" + end + test "returns feed with public and unlisted activities", %{conn: conn} do user = insert(:user)diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs@@ -19,7 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do import Pleroma.Factory setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs@@ -152,4 +152,46 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do } ] = result["rules"] end + + test "translation languages matrix", %{conn: conn} do + clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + assert %{"en" => ["pl"], "pl" => ["en"]} = + conn + |> get("/api/v1/instance/translation_languages") + |> json_response_and_validate_schema(200) + end + + test "base_urls in pleroma metadata", %{conn: conn} do + media_proxy_base_url = "https://media.example.org" + upload_base_url = "https://uploads.example.org" + + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :base_url], media_proxy_base_url) + clear_config([Pleroma.Upload, :base_url], upload_base_url) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + assert result["pleroma"]["metadata"]["base_urls"]["media_proxy"] == media_proxy_base_url + assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url + + # Test when media_proxy is disabled + clear_config([:media_proxy, :enabled], false) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy") + assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url + + # Test when upload base_url is not set + clear_config([Pleroma.Upload, :base_url], nil) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy") + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "upload") + end enddiff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs@@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do conn |> put_req_header("content-type", "multipart/form-data") |> post("/api/v2/media", %{"file" => image, "description" => desc}) - |> json_response_and_validate_schema(202) + |> json_response_and_validate_schema(200) assert media_id = response["id"] @@ -111,7 +111,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do "file" => large_binary, "description" => desc }) - |> json_response_and_validate_schema(202) + |> json_response_and_validate_schema(200) assert media_id = response["id"] @@ -227,4 +227,93 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do |> json_response_and_validate_schema(403) end end + + describe "Content-Type sanitization" do + setup do: oauth_access(["write:media", "read:media"]) + + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + config = + Pleroma.Config.get([Pleroma.Upload]) + |> Keyword.put(:uploader, Pleroma.Uploaders.Local) + + clear_config([Pleroma.Upload], config) + clear_config([Pleroma.Upload, :allowed_mime_types], ["image", "audio", "video"]) + + # Create a file with a malicious content type and dangerous extension + malicious_file = %Plug.Upload{ + content_type: "application/activity+json", + path: Path.absname("test/fixtures/image.jpg"), + # JSON extension to make MIME.from_path detect application/json + filename: "malicious.json" + } + + [malicious_file: malicious_file] + end + + test "sanitizes malicious content types when serving media", %{ + conn: conn, + malicious_file: malicious_file + } do + # First upload the file with the malicious content type + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{"file" => malicious_file}) + |> json_response_and_validate_schema(:ok) + + # Get the file URL from the response + url = media["url"] + + # Now make a direct request to the media URL and check the content-type header + response = + build_conn() + |> get(URI.parse(url).path) + + # Find the content-type header + content_type_header = + Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end) + + # The server should detect the application/json MIME type from the .json extension + # and replace it with application/octet-stream since it's not in allowed_mime_types + assert content_type_header == {"content-type", "application/octet-stream"} + + # Verify that the file was still served correctly + assert response.status == 200 + end + + test "allows safe content types", %{conn: conn} do + safe_image = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "safe_image.jpg" + } + + # Upload a file with a safe content type + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{"file" => safe_image}) + |> json_response_and_validate_schema(:ok) + + # Get the file URL from the response + url = media["url"] + + # Make a direct request to the media URL and check the content-type header + response = + build_conn() + |> get(URI.parse(url).path) + + # The server should preserve the image/jpeg MIME type since it's allowed + content_type_header = + Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end) + + assert content_type_header == {"content-type", "image/jpeg"} + + # Verify that the file was served correctly + assert response.status == 200 + end + end enddiff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do import Pleroma.Factory setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs@@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.PollControllerTest do + use Oban.Testing, repo: Pleroma.Repo use Pleroma.Web.ConnCase, async: true alias Pleroma.Object @@ -27,6 +28,33 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do response = json_response_and_validate_schema(conn, 200) id = to_string(object.id) assert %{"id" => ^id, "expired" => false, "multiple" => false} = response + + # Local activities should not generate an Oban job to refresh + assert activity.local + + refute_enqueued( + worker: Pleroma.Workers.PollWorker, + args: %{"op" => "refresh", "activity_id" => activity.id} + ) + end + + test "creates an oban job to refresh poll if activity is remote", %{conn: conn} do + user = insert(:user, local: false) + question = insert(:question, user: user) + activity = insert(:question_activity, question: question, local: false) + + # Ensure this is not represented as a local activity + refute activity.local + + object = Object.normalize(activity, fetch: false) + + get(conn, "/api/v1/polls/#{object.id}") + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PollWorker, + args: %{"op" => "refresh", "activity_id" => activity.id} + ) end test "does not expose polls for private statuses", %{conn: conn} dodiff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs@@ -7,14 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do alias Pleroma.Object alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint import Pleroma.Factory import ExUnit.CaptureLog import Tesla.Mock import Mock setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok end @@ -66,9 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do [account | _] = results["accounts"] assert account["id"] == to_string(user_three.id) - assert results["hashtags"] == [ - %{"name" => "private", "url" => "#{Endpoint.url()}/tag/private"} - ] + assert results["hashtags"] == [] [status] = results["statuses"] assert status["id"] == to_string(activity.id) @@ -77,9 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do get(conn, "/api/v2/search?q=天子") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "天子", "url" => "#{Endpoint.url()}/tag/天子"} - ] + assert results["hashtags"] == [] [status] = results["statuses"] assert status["id"] == to_string(activity.id) @@ -130,84 +125,97 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do assert [] = results["statuses"] end - test "constructs hashtags from search query", %{conn: conn} do + test "returns empty results when no hashtags match", %{conn: conn} do results = conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") + |> get("/api/v2/search?#{URI.encode_query(%{q: "nonexistent"})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "explicit", "url" => "#{Endpoint.url()}/tag/explicit"}, - %{"name" => "hashtags", "url" => "#{Endpoint.url()}/tag/hashtags"} - ] + assert results["hashtags"] == [] + end + + test "searches hashtags by multiple words in query", %{conn: conn} do + user = insert(:user) + + {:ok, _activity1} = CommonAPI.post(user, %{status: "This is my new #computer"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "Check out this #laptop"}) + {:ok, _activity3} = CommonAPI.post(user, %{status: "My #desktop setup"}) + {:ok, _activity4} = CommonAPI.post(user, %{status: "New #phone arrived"}) results = conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") + |> get("/api/v2/search?#{URI.encode_query(%{q: "new computer"})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "john", "url" => "#{Endpoint.url()}/tag/john"}, - %{"name" => "doe", "url" => "#{Endpoint.url()}/tag/doe"}, - %{"name" => "JohnDoe", "url" => "#{Endpoint.url()}/tag/JohnDoe"} - ] + hashtag_names = Enum.map(results["hashtags"], & &1["name"]) + assert "computer" in hashtag_names + refute "laptop" in hashtag_names + refute "desktop" in hashtag_names + refute "phone" in hashtag_names results = conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") + |> get("/api/v2/search?#{URI.encode_query(%{q: "computer laptop"})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "accident", "url" => "#{Endpoint.url()}/tag/accident"}, - %{"name" => "prone", "url" => "#{Endpoint.url()}/tag/prone"}, - %{"name" => "AccidentProne", "url" => "#{Endpoint.url()}/tag/AccidentProne"} - ] + hashtag_names = Enum.map(results["hashtags"], & &1["name"]) + assert "computer" in hashtag_names + assert "laptop" in hashtag_names + refute "desktop" in hashtag_names + refute "phone" in hashtag_names + end + + test "supports pagination of hashtags search results", %{conn: conn} do + user = insert(:user) + + {:ok, _activity1} = CommonAPI.post(user, %{status: "First #alpha hashtag"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "Second #beta hashtag"}) + {:ok, _activity3} = CommonAPI.post(user, %{status: "Third #gamma hashtag"}) + {:ok, _activity4} = CommonAPI.post(user, %{status: "Fourth #delta hashtag"}) results = conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") + |> get("/api/v2/search?#{URI.encode_query(%{q: "a", limit: 2, offset: 1})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "shpuld", "url" => "#{Endpoint.url()}/tag/shpuld"} - ] + hashtag_names = Enum.map(results["hashtags"], & &1["name"]) + + # Should return 2 hashtags (alpha, beta, gamma, delta all contain 'a') + # With offset 1, we skip the first one, so we get 2 of the remaining 3 + assert length(hashtag_names) == 2 + assert Enum.all?(hashtag_names, &String.contains?(&1, "a")) + end + + test "searches real hashtags from database", %{conn: conn} do + user = insert(:user) + + {:ok, _activity1} = CommonAPI.post(user, %{status: "Check out this #car"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "Fast #racecar on the track"}) + {:ok, _activity3} = CommonAPI.post(user, %{status: "NASCAR #nascar racing"}) results = conn - |> get( - "/api/v2/search?#{URI.encode_query(%{q: "https://www.washingtonpost.com/sports/2020/06/10/" <> "nascar-ban-display-confederate-flag-all-events-properties/"})}" - ) + |> get("/api/v2/search?#{URI.encode_query(%{q: "car"})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "nascar", "url" => "#{Endpoint.url()}/tag/nascar"}, - %{"name" => "ban", "url" => "#{Endpoint.url()}/tag/ban"}, - %{"name" => "display", "url" => "#{Endpoint.url()}/tag/display"}, - %{"name" => "confederate", "url" => "#{Endpoint.url()}/tag/confederate"}, - %{"name" => "flag", "url" => "#{Endpoint.url()}/tag/flag"}, - %{"name" => "all", "url" => "#{Endpoint.url()}/tag/all"}, - %{"name" => "events", "url" => "#{Endpoint.url()}/tag/events"}, - %{"name" => "properties", "url" => "#{Endpoint.url()}/tag/properties"}, - %{ - "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", - "url" => - "#{Endpoint.url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" - } - ] - end + hashtag_names = Enum.map(results["hashtags"], & &1["name"]) - test "supports pagination of hashtags search results", %{conn: conn} do + # Should return car, racecar, and nascar since they all contain "car" + assert "car" in hashtag_names + assert "racecar" in hashtag_names + assert "nascar" in hashtag_names + + # Search for "race" - should return racecar results = conn - |> get( - "/api/v2/search?#{URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1})}" - ) + |> get("/api/v2/search?#{URI.encode_query(%{q: "race"})}") |> json_response_and_validate_schema(200) - assert results["hashtags"] == [ - %{"name" => "text", "url" => "#{Endpoint.url()}/tag/text"}, - %{"name" => "with", "url" => "#{Endpoint.url()}/tag/with"} - ] + hashtag_names = Enum.map(results["hashtags"], & &1["name"]) + + assert "racecar" in hashtag_names + refute "car" in hashtag_names + refute "nascar" in hashtag_names end test "excludes a blocked users from search results", %{conn: conn} do @@ -314,7 +322,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do [account | _] = results["accounts"] assert account["id"] == to_string(user_three.id) - assert results["hashtags"] == ["2hu"] + assert results["hashtags"] == [] [status] = results["statuses"] assert status["id"] == to_string(activity.id)diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs@@ -2483,4 +2483,62 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:not_found) end end + + describe "translating statuses" do + setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + test "it translates a status to user language" do + user = insert(:user, language: "fr") + %{conn: conn} = oauth_access(["read:statuses"], user: user) + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{ + status: "Cześć!", + visibility: "public", + language: "pl" + }) + + response = + conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(200) + + assert response == %{ + "content" => "!ćśezC", + "detected_source_language" => "pl", + "provider" => "TranslationMock" + } + end + + test "it returns an error if no target language provided" do + %{conn: conn} = oauth_access(["read:statuses"]) + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{ + status: "Cześć!", + language: "pl" + }) + + assert conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(400) + end + + test "it doesn't translate non-public statuses" do + %{conn: conn, user: user} = oauth_access(["read:statuses"]) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Cześć!", + visibility: "private", + language: "pl" + }) + + assert conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(404) + end + end enddiff --git a/test/pleroma/web/mastodon_api/controllers/tag_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/tag_controller_test.exs@@ -0,0 +1,159 @@ +defmodule Pleroma.Web.MastodonAPI.TagControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Tesla.Mock + + alias Pleroma.User + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "GET /api/v1/tags/:id" do + test "returns 200 with tag" do + %{user: user, conn: conn} = oauth_access(["read"]) + + tag = insert(:hashtag, name: "jubjub") + {:ok, _user} = User.follow_hashtag(user, tag) + + response = + conn + |> get("/api/v1/tags/jubjub") + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "jubjub", + "url" => "http://localhost:4001/tags/jubjub", + "history" => [], + "following" => true + } = response + end + + test "returns 404 with unknown tag" do + %{conn: conn} = oauth_access(["read"]) + + conn + |> get("/api/v1/tags/jubjub") + |> json_response_and_validate_schema(404) + end + end + + describe "POST /api/v1/tags/:id/follow" do + test "should follow a hashtag" do + %{user: user, conn: conn} = oauth_access(["write:follows"]) + hashtag = insert(:hashtag, name: "jubjub") + + response = + conn + |> post("/api/v1/tags/jubjub/follow") + |> json_response_and_validate_schema(200) + + assert response["following"] == true + user = User.get_cached_by_ap_id(user.ap_id) + assert User.following_hashtag?(user, hashtag) + end + + test "should 404 if hashtag doesn't exist" do + %{conn: conn} = oauth_access(["write:follows"]) + + response = + conn + |> post("/api/v1/tags/rubrub/follow") + |> json_response_and_validate_schema(404) + + assert response["error"] == "Hashtag not found" + end + end + + describe "POST /api/v1/tags/:id/unfollow" do + test "should unfollow a hashtag" do + %{user: user, conn: conn} = oauth_access(["write:follows"]) + hashtag = insert(:hashtag, name: "jubjub") + {:ok, user} = User.follow_hashtag(user, hashtag) + + response = + conn + |> post("/api/v1/tags/jubjub/unfollow") + |> json_response_and_validate_schema(200) + + assert response["following"] == false + user = User.get_cached_by_ap_id(user.ap_id) + refute User.following_hashtag?(user, hashtag) + end + + test "should 404 if hashtag doesn't exist" do + %{conn: conn} = oauth_access(["write:follows"]) + + response = + conn + |> post("/api/v1/tags/rubrub/unfollow") + |> json_response_and_validate_schema(404) + + assert response["error"] == "Hashtag not found" + end + end + + describe "GET /api/v1/followed_tags" do + test "should list followed tags" do + %{user: user, conn: conn} = oauth_access(["read:follows"]) + + response = + conn + |> get("/api/v1/followed_tags") + |> json_response_and_validate_schema(200) + + assert Enum.empty?(response) + + hashtag = insert(:hashtag, name: "jubjub") + {:ok, _user} = User.follow_hashtag(user, hashtag) + + response = + conn + |> get("/api/v1/followed_tags") + |> json_response_and_validate_schema(200) + + assert [%{"name" => "jubjub"}] = response + end + + test "should include a link header to paginate" do + %{user: user, conn: conn} = oauth_access(["read:follows"]) + + for i <- 1..21 do + hashtag = insert(:hashtag, name: "jubjub#{i}}") + {:ok, _user} = User.follow_hashtag(user, hashtag) + end + + response = + conn + |> get("/api/v1/followed_tags") + + json = json_response_and_validate_schema(response, 200) + assert Enum.count(json) == 20 + assert [link_header] = get_resp_header(response, "link") + assert link_header =~ "rel=\"next\"" + next_link = extract_next_link_header(link_header) + + response = + conn + |> get(next_link) + |> json_response_and_validate_schema(200) + + assert Enum.count(response) == 1 + end + + test "should refuse access without read:follows scope" do + %{conn: conn} = oauth_access(["write"]) + + conn + |> get("/api/v1/followed_tags") + |> json_response_and_validate_schema(403) + end + end + + defp extract_next_link_header(header) do + [_, next_link] = Regex.run(~r{<(?<next_link>.*)>; rel="next"}, header) + next_link + end +enddiff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do import Pleroma.Factory setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs@@ -951,6 +951,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert status.edited_at end + test "it shows post language" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "Szczęść Boże", language: "pl"}) + + status = StatusView.render("show.json", activity: post) + + assert status.language == "pl" + end + + test "doesn't show post language if it's 'und'" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "sdifjogijodfg", language: "und"}) + + status = StatusView.render("show.json", activity: post) + + assert status.language == nil + end + test "with a source object" do note = insert(:note,diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs@@ -248,8 +248,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do response = get(conn, url) - assert response.status == 302 - assert redirected_to(response) == media_proxy_url + assert response.status == 301 + assert redirected_to(response, 301) == media_proxy_url end test "with `static` param and non-GIF image preview requested, " <> @@ -290,8 +290,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do response = get(conn, url) - assert response.status == 302 - assert redirected_to(response) == media_proxy_url + assert response.status == 301 + assert redirected_to(response, 301) == media_proxy_url end test "thumbnails PNG images into PNG", %{ @@ -356,5 +356,32 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do assert response.status == 302 assert redirected_to(response) == media_proxy_url end + + test "redirects to media proxy URI with 301 when image is too small for preview", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + clear_config([:media_preview_proxy], + enabled: true, + min_content_length: 1000, + image_quality: 85, + thumbnail_max_width: 100, + thumbnail_max_height: 100 + ) + + Tesla.Mock.mock(fn + %{method: :head, url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/png"}, {"content-length", "500"}] + } + end) + + response = get(conn, url) + assert response.status == 301 + assert redirected_to(response, 301) == media_proxy_url + end end enddiff --git a/test/pleroma/web/metadata/providers/activity_pub_test.exs b/test/pleroma/web/metadata/providers/activity_pub_test.exs@@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ActivityPubTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Metadata.Providers.ActivityPub + + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + + test "it renders a link for user info" do + user = insert(:user) + res = ActivityPub.build_tags(%{user: user}) + + assert res == [ + {:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []} + ] + end + + test "it renders a link for a post" do + user = insert(:user) + {:ok, %{id: activity_id, object: object}} = CommonAPI.post(user, %{status: "hi"}) + + result = ActivityPub.build_tags(%{object: object, user: user, activity_id: activity_id}) + + assert [ + {:link, + [rel: "alternate", type: "application/activity+json", href: object.data["id"]], []} + ] == result + end + + test "it returns an empty array for anything else" do + result = ActivityPub.build_tags(%{}) + + assert result == [] + end +enddiff --git a/test/pleroma/web/metadata/providers/feed_test.exs b/test/pleroma/web/metadata/providers/feed_test.exs@@ -15,4 +15,10 @@ defmodule Pleroma.Web.Metadata.Providers.FeedTest do [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []} ] end + + test "it doesn't render a link to remote user's feed" do + user = insert(:user, nickname: "lain@lain.com", local: false) + + assert Feed.build_tags(%{user: user}) == [] + end enddiff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs@@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.Metadata.Providers.OpenGraph + alias Pleroma.Web.Metadata.Utils setup do ConfigMock @@ -197,4 +198,58 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do "http://localhost:4001/proxy/preview/LzAnlke-l5oZbNzWsrHfprX1rGw/aHR0cHM6Ly9wbGVyb21hLmdvdi9hYm91dC9qdWNoZS53ZWJt/juche.webm" ], []} in result end + + test "meta tag ordering matches attachment order" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "https://example.com/first.png", + "height" => 1024, + "width" => 1280 + } + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "https://example.com/second.png", + "height" => 1024, + "width" => 1280 + } + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert [ + {:meta, [property: "og:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "og:url", content: "https://pleroma.gov/objects/whatever"], []}, + {:meta, [property: "og:description", content: "pleroma in a nutshell"], []}, + {:meta, [property: "og:type", content: "article"], []}, + {:meta, [property: "og:image", content: "https://example.com/first.png"], []}, + {:meta, [property: "og:image:alt", content: nil], []}, + {:meta, [property: "og:image:width", content: "1280"], []}, + {:meta, [property: "og:image:height", content: "1024"], []}, + {:meta, [property: "og:image", content: "https://example.com/second.png"], []}, + {:meta, [property: "og:image:alt", content: nil], []}, + {:meta, [property: "og:image:width", content: "1280"], []}, + {:meta, [property: "og:image:height", content: "1024"], []} + ] == result + end enddiff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs@@ -202,4 +202,58 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do {:meta, [name: "twitter:player:stream:content_type", content: "video/webm"], []} ] == result end + + test "meta tag ordering matches attachment order" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "https://example.com/first.png", + "height" => 1024, + "width" => 1280 + } + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "https://example.com/second.png", + "height" => 1024, + "width" => 1280 + } + ] + } + ] + } + }) + + result = TwitterCard.build_tags(%{object: note, activity_id: note.data["id"], user: user}) + + assert [ + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, + {:meta, [name: "twitter:image", content: "https://example.com/first.png"], []}, + {:meta, [name: "twitter:image:alt", content: ""], []}, + {:meta, [name: "twitter:player:width", content: "1280"], []}, + {:meta, [name: "twitter:player:height", content: "1024"], []}, + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, + {:meta, [name: "twitter:image", content: "https://example.com/second.png"], []}, + {:meta, [name: "twitter:image:alt", content: ""], []}, + {:meta, [name: "twitter:player:width", content: "1280"], []}, + {:meta, [name: "twitter:player:height", content: "1024"], []} + ] == result + end enddiff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs@@ -58,16 +58,28 @@ defmodule Pleroma.Web.OAuth.AppTest do attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} {:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"]) + # backdate the old app so it's within the threshold for being cleaned up + one_hour_ago = DateTime.add(DateTime.utc_now(), -3600) + + {:ok, _} = + "UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2" + |> Pleroma.Repo.query([one_hour_ago, old_app.id]) + + # Create the new app after backdating the old one attrs = %{client_name: "PleromaFE", redirect_uris: "."} {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) - # backdate the old app so it's within the threshold for being cleaned up + # Ensure the new app has a recent timestamp + now = DateTime.utc_now() + {:ok, _} = - "UPDATE apps SET inserted_at = now() - interval '1 hour' WHERE id = #{old_app.id}" - |> Pleroma.Repo.query() + "UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2" + |> Pleroma.Repo.query([now, app.id]) App.remove_orphans() - assert [app] == Pleroma.Repo.all(App) + assert [returned_app] = Pleroma.Repo.all(App) + assert returned_app.client_name == "PleromaFE" + assert returned_app.id == app.id end enddiff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs@@ -292,10 +292,14 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do User.endorse(user1, user2) User.endorse(user1, user3) - [%{"id" => ^id2}, %{"id" => ^id3}] = + response = conn |> get("/api/v1/pleroma/accounts/#{id1}/endorsements") |> json_response_and_validate_schema(200) + + assert length(response) == 2 + assert Enum.any?(response, fn user -> user["id"] == id2 end) + assert Enum.any?(response, fn user -> user["id"] == id3 end) end test "returns 404 error when specified user is not exist", %{conn: conn} dodiff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_download_zip_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_download_zip_test.exs@@ -0,0 +1,371 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerDownloadZipTest do + use Pleroma.Web.ConnCase, async: false + + import Tesla.Mock + import Pleroma.Factory + + setup_all do + # Create a base temp directory for this test module + base_temp_dir = Path.join(System.tmp_dir!(), "emoji_test_#{Ecto.UUID.generate()}") + + # Clean up when all tests in module are done + on_exit(fn -> + File.rm_rf!(base_temp_dir) + end) + + {:ok, %{base_temp_dir: base_temp_dir}} + end + + setup %{base_temp_dir: base_temp_dir} do + # Create a unique subdirectory for each test + test_id = Ecto.UUID.generate() + temp_dir = Path.join(base_temp_dir, test_id) + emoji_dir = Path.join(temp_dir, "emoji") + + # Create the directory structure + File.mkdir_p!(emoji_dir) + + # Configure this test to use the temp directory + clear_config([:instance, :static_dir], temp_dir) + + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + + {:ok, %{admin_conn: admin_conn, emoji_path: emoji_dir}} + end + + describe "POST /api/pleroma/emoji/packs/download_zip" do + setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) + end + + test "creates pack from uploaded ZIP file", %{admin_conn: admin_conn, emoji_path: emoji_path} do + # Create a test ZIP file with emojis + {:ok, zip_path} = create_test_emoji_zip() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack.zip" + } + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_zip_pack", + file: upload + }) + |> json_response_and_validate_schema(200) == "ok" + + # Verify pack was created + assert File.exists?("#{emoji_path}/test_zip_pack/pack.json") + assert File.exists?("#{emoji_path}/test_zip_pack/test_emoji.png") + + # Verify pack.json contents + {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack/pack.json") + pack_data = Jason.decode!(pack_json) + + assert pack_data["files"]["test_emoji"] == "test_emoji.png" + assert pack_data["pack"]["src_sha256"] != nil + + # Clean up + File.rm!(zip_path) + end + + test "creates pack from URL", %{admin_conn: admin_conn, emoji_path: emoji_path} do + # Mock HTTP request to download ZIP + {:ok, zip_path} = create_test_emoji_zip() + {:ok, zip_data} = File.read(zip_path) + + mock(fn + %{method: :get, url: "https://example.com/emoji_pack.zip"} -> + %Tesla.Env{status: 200, body: zip_data} + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_zip_pack_url", + url: "https://example.com/emoji_pack.zip" + }) + |> json_response_and_validate_schema(200) == "ok" + + # Verify pack was created + assert File.exists?("#{emoji_path}/test_zip_pack_url/pack.json") + assert File.exists?("#{emoji_path}/test_zip_pack_url/test_emoji.png") + + # Verify pack.json has URL as source + {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack_url/pack.json") + pack_data = Jason.decode!(pack_json) + + assert pack_data["pack"]["src"] == "https://example.com/emoji_pack.zip" + assert pack_data["pack"]["src_sha256"] != nil + + # Clean up + File.rm!(zip_path) + end + + test "refuses to overwrite existing pack", %{admin_conn: admin_conn, emoji_path: emoji_path} do + # Create existing pack + pack_path = Path.join(emoji_path, "test_zip_pack") + File.mkdir_p!(pack_path) + File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(%{files: %{}})) + + {:ok, zip_path} = create_test_emoji_zip() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack.zip" + } + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_zip_pack", + file: upload + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Pack already exists, refusing to import test_zip_pack" + } + + # Clean up + File.rm!(zip_path) + end + + test "handles invalid ZIP file", %{admin_conn: admin_conn} do + # Create invalid ZIP file + invalid_zip_path = Path.join(System.tmp_dir!(), "invalid.zip") + File.write!(invalid_zip_path, "not a zip file") + + upload = %Plug.Upload{ + content_type: "application/zip", + path: invalid_zip_path, + filename: "invalid.zip" + } + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_invalid_pack", + file: upload + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Could not unzip pack" + } + + # Clean up + File.rm!(invalid_zip_path) + end + + test "handles URL download failure", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/bad_pack.zip"} -> + %Tesla.Env{status: 404, body: "Not found"} + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_bad_url_pack", + url: "https://example.com/bad_pack.zip" + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Could not download pack" + } + end + + test "requires either file or URL parameter", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_no_source_pack" + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Neither file nor URL was present in the request" + } + end + + test "returns error when pack name is empty", %{admin_conn: admin_conn} do + {:ok, zip_path} = create_test_emoji_zip() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack.zip" + } + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "", + file: upload + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Pack name cannot be empty" + } + + # Clean up + File.rm!(zip_path) + end + + test "returns error when unable to create pack directory", %{ + admin_conn: admin_conn, + emoji_path: emoji_path + } do + # Make the emoji directory read-only to trigger mkdir_p failure + + # Save original permissions + {:ok, %{mode: original_mode}} = File.stat(emoji_path) + + # Make emoji directory read-only (no write permission) + File.chmod!(emoji_path, 0o555) + + {:ok, zip_path} = create_test_emoji_zip() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack.zip" + } + + # Try to create a pack in the read-only emoji directory + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_readonly_pack", + file: upload + }) + |> json_response_and_validate_schema(400) == %{ + "error" => "Could not create the pack directory" + } + + # Clean up - restore original permissions + File.chmod!(emoji_path, original_mode) + File.rm!(zip_path) + end + + test "preserves existing pack.json if present in ZIP", %{ + admin_conn: admin_conn, + emoji_path: emoji_path + } do + # Create ZIP with pack.json + {:ok, zip_path} = create_test_emoji_zip_with_pack_json() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack_with_json.zip" + } + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: "test_zip_pack_with_json", + file: upload + }) + |> json_response_and_validate_schema(200) == "ok" + + # Verify original pack.json was preserved + {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack_with_json/pack.json") + pack_data = Jason.decode!(pack_json) + + assert pack_data["pack"]["description"] == "Test pack from ZIP" + assert pack_data["pack"]["license"] == "Test License" + + # Clean up + File.rm!(zip_path) + end + + test "rejects malicious pack names", %{admin_conn: admin_conn} do + {:ok, zip_path} = create_test_emoji_zip() + + upload = %Plug.Upload{ + content_type: "application/zip", + path: zip_path, + filename: "test_pack.zip" + } + + # Test path traversal attempts + malicious_names = ["../evil", "../../evil", ".", "..", "evil/../../../etc"] + + Enum.each(malicious_names, fn name -> + assert_raise RuntimeError, ~r/Invalid or malicious pack name/, fn -> + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download_zip", %{ + name: name, + file: upload + }) + end + end) + + # Clean up + File.rm!(zip_path) + end + end + + defp create_test_emoji_zip do + tmp_dir = System.tmp_dir!() + zip_path = Path.join(tmp_dir, "test_emoji_pack_#{:rand.uniform(10000)}.zip") + + # 1x1 pixel PNG + png_data = + Base.decode64!( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + ) + + files = [ + {~c"test_emoji.png", png_data}, + # Will be treated as GIF based on extension + {~c"another_emoji.gif", png_data} + ] + + {:ok, {_name, zip_binary}} = :zip.zip(~c"test_pack.zip", files, [:memory]) + File.write!(zip_path, zip_binary) + + {:ok, zip_path} + end + + defp create_test_emoji_zip_with_pack_json do + tmp_dir = System.tmp_dir!() + zip_path = Path.join(tmp_dir, "test_emoji_pack_json_#{:rand.uniform(10000)}.zip") + + png_data = + Base.decode64!( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + ) + + pack_json = + Jason.encode!(%{ + pack: %{ + description: "Test pack from ZIP", + license: "Test License" + }, + files: %{ + "test_emoji" => "test_emoji.png" + } + }) + + files = [ + {~c"test_emoji.png", png_data}, + {~c"pack.json", pack_json} + ] + + {:ok, {_name, zip_binary}} = :zip.zip(~c"test_pack.zip", files, [:memory]) + File.write!(zip_path, zip_binary) + + {:ok, zip_path} + end +enddiff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs@@ -14,7 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do import Pleroma.Factory setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs@@ -7,16 +7,11 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do alias Pleroma.Instances - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - setup do constant = "http://consistently-unreachable.name/" - eventual = "http://eventually-unreachable.com/path" {:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} = - Instances.set_consistently_unreachable(constant) - - _eventual_unreachable = Instances.set_unreachable(eventual) + Instances.set_unreachable(constant) %{constant_unreachable: constant_unreachable, constant: constant} enddiff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs@@ -19,10 +19,33 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do "artist" => "lain", "album" => "lain radio", "length" => "180000", - "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) - assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) + assert %{ + "title" => "lain radio episode 1", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + } = json_response_and_validate_schema(conn, 200) + end + + test "external_link fallback" do + %{conn: conn} = oauth_access(["write"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/scrobble", %{ + "title" => "lain radio episode 2", + "artist" => "lain", + "album" => "lain radio", + "length" => "180000", + "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + }) + + assert %{ + "title" => "lain radio episode 2", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + } = json_response_and_validate_schema(conn, 200) end end @@ -35,7 +58,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 1", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) {:ok, _activity} = @@ -43,7 +66,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 2", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" }) {:ok, _activity} = @@ -51,7 +74,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 3", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" }) conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles")diff --git a/test/pleroma/web/plugs/cache_test.exs b/test/pleroma/web/plugs/cache_test.exs@@ -5,7 +5,8 @@ defmodule Pleroma.Web.Plugs.CacheTest do # Relies on Cachex, has to stay synchronous use Pleroma.DataCase - use Plug.Test + import Plug.Conn + import Plug.Test alias Pleroma.Web.Plugs.Cachediff --git a/test/pleroma/web/plugs/digest_plug_test.exs b/test/pleroma/web/plugs/digest_plug_test.exs@@ -4,7 +4,8 @@ defmodule Pleroma.Web.Plugs.DigestPlugTest do use ExUnit.Case, async: true - use Plug.Test + import Plug.Conn + import Plug.Test test "digest algorithm is taken from digest header" do body = "{\"hello\": \"world\"}"diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -38,7 +38,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") @@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/pleroma/admin/") @@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!("#{path}/proxy/rr/ss") + Pleroma.Backports.mkdir_p!("#{path}/proxy/rr/ss") File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image") ConfigMockdiff --git a/test/pleroma/web/plugs/idempotency_plug_test.exs b/test/pleroma/web/plugs/idempotency_plug_test.exs@@ -5,7 +5,8 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do # Relies on Cachex, has to stay synchronous use Pleroma.DataCase - use Plug.Test + import Plug.Conn + import Plug.Test alias Pleroma.Web.Plugs.IdempotencyPlug alias Plug.Conndiff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs@@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -34,7 +34,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do refute html_response(bundled_index, 200) == "from frontend plug" path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") @@ -62,4 +62,79 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do index = get(build_conn(), "/static/kaniini.html") assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" end + + test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do + # Create a file with a potentially dangerous extension (.json) + # This mimics an attacker trying to serve ActivityPub JSON with a static file + File.mkdir!(@dir <> "/static") + File.write!(@dir <> "/static/malicious.json", "{\"type\": \"ActivityPub\"}") + + conn = get(build_conn(), "/static/malicious.json") + + assert conn.status == 200 + + content_type = + Enum.find_value(conn.resp_headers, fn + {"content-type", value} -> value + _ -> nil + end) + + assert content_type == "application/json" + + File.write!(@dir <> "/static/safe.jpg", "fake image data") + + conn = get(build_conn(), "/static/safe.jpg") + + assert conn.status == 200 + + # Get the content-type + content_type = + Enum.find_value(conn.resp_headers, fn + {"content-type", value} -> value + _ -> nil + end) + + assert content_type == "image/jpeg" + end + + test "always sanitizes emojis to images" do + File.mkdir!(@dir <> "/emoji") + File.write!(@dir <> "/emoji/malicious.html", "<script>HACKED</script>") + + # Request the malicious file + conn = get(build_conn(), "/emoji/malicious.html") + + # Verify the file was served (status 200) + assert conn.status == 200 + + # The content should be served, but with a sanitized content-type + content_type = + Enum.find_value(conn.resp_headers, fn + {"content-type", value} -> value + _ -> nil + end) + + # It should have been sanitized to application/octet-stream because "application" + # is not in the allowed_mime_types list + assert content_type == "application/octet-stream" + + # Create a file with an allowed extension (.jpg) + File.write!(@dir <> "/emoji/safe.jpg", "fake image data") + + # Request the safe file + conn = get(build_conn(), "/emoji/safe.jpg") + + # Verify the file was served (status 200) + assert conn.status == 200 + + # Get the content-type + content_type = + Enum.find_value(conn.resp_headers, fn + {"content-type", value} -> value + _ -> nil + end) + + # It should be preserved because "image" is in the allowed_mime_types list + assert content_type == "image/jpeg" + end enddiff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs@@ -4,7 +4,8 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do use ExUnit.Case - use Plug.Test + import Plug.Conn + import Plug.Test alias Pleroma.Web.Plugs.RemoteIpdiff --git a/test/pleroma/web/plugs/set_format_plug_test.exs b/test/pleroma/web/plugs/set_format_plug_test.exs@@ -4,7 +4,8 @@ defmodule Pleroma.Web.Plugs.SetFormatPlugTest do use ExUnit.Case, async: true - use Plug.Test + import Plug.Conn + import Plug.Test alias Pleroma.Web.Plugs.SetFormatPlugdiff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs@@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.SetLocalePlugTest do use ExUnit.Case, async: true - use Plug.Test + import Plug.Test alias Pleroma.Web.Plugs.SetLocalePlug alias Plug.Conndiff --git a/test/pleroma/web/plugs/uploaded_media_test.exs b/test/pleroma/web/plugs/uploaded_media_test.exs@@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UploadedMediaTest do + use ExUnit.Case, async: true + + alias Pleroma.Web.Plugs.Utils + + describe "content-type sanitization with Utils.get_safe_mime_type/2" do + test "it allows safe MIME types" do + opts = %{allowed_mime_types: ["image", "audio", "video"]} + + assert Utils.get_safe_mime_type(opts, "image/jpeg") == "image/jpeg" + assert Utils.get_safe_mime_type(opts, "audio/mpeg") == "audio/mpeg" + assert Utils.get_safe_mime_type(opts, "video/mp4") == "video/mp4" + end + + test "it sanitizes potentially dangerous content-types" do + opts = %{allowed_mime_types: ["image", "audio", "video"]} + + assert Utils.get_safe_mime_type(opts, "application/activity+json") == + "application/octet-stream" + + assert Utils.get_safe_mime_type(opts, "text/html") == "application/octet-stream" + + assert Utils.get_safe_mime_type(opts, "application/javascript") == + "application/octet-stream" + end + + test "it sanitizes ActivityPub content types" do + opts = %{allowed_mime_types: ["image", "audio", "video"]} + + assert Utils.get_safe_mime_type(opts, "application/activity+json") == + "application/octet-stream" + + assert Utils.get_safe_mime_type(opts, "application/ld+json") == "application/octet-stream" + assert Utils.get_safe_mime_type(opts, "application/jrd+json") == "application/octet-stream" + end + + test "it sanitizes other potentially dangerous types" do + opts = %{allowed_mime_types: ["image", "audio", "video"]} + + assert Utils.get_safe_mime_type(opts, "text/html") == "application/octet-stream" + + assert Utils.get_safe_mime_type(opts, "application/javascript") == + "application/octet-stream" + + assert Utils.get_safe_mime_type(opts, "text/javascript") == "application/octet-stream" + assert Utils.get_safe_mime_type(opts, "application/xhtml+xml") == "application/octet-stream" + end + end +enddiff --git a/test/pleroma/web/plugs/user_tracking_plug_test.exs b/test/pleroma/web/plugs/user_tracking_plug_test.exs@@ -21,8 +21,12 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do |> assign(:user, user) |> UserTrackingPlug.call(%{}) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] end test "doesn't update last_active_at if it was updated recently", %{conn: conn} do @@ -38,7 +42,7 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do |> assign(:user, user) |> UserTrackingPlug.call(%{}) - assert user.last_active_at == last_active_at + assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq end test "skips updating last_active_at if user ID is nil", %{conn: conn} dodiff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs@@ -83,4 +83,23 @@ defmodule Pleroma.Web.RichMedia.CardTest do Card.get_by_activity(activity) ) end + + test "refuses to crawl URL in activity from ignored host/domain" do + clear_config([:rich_media, :ignore_hosts], ["example.com"]) + + user = insert(:user) + + url = "https://example.com/ogp" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "[test](#{url})", + content_type: "text/markdown" + }) + + refute_enqueued( + worker: RichMediaWorker, + args: %{"url" => url, "activity_id" => activity.id} + ) + end enddiff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs@@ -54,7 +54,6 @@ defmodule Pleroma.Web.RichMedia.ParserTest do {:ok, %{ "card" => "summary", - "site" => "@flickr", "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", "title" => "Small Island Developing States Photo Submission", "description" => "View the album on Flickr.", @@ -62,6 +61,13 @@ defmodule Pleroma.Web.RichMedia.ParserTest do }} end + test "truncates title and description fields" do + {:ok, parsed} = Parser.parse("https://instagram.com/longtext") + + assert String.length(parsed["title"]) == 120 + assert String.length(parsed["description"]) == 200 + end + test "parses OEmbed and filters HTML tags" do assert Parser.parse("https://example.com/oembed") == {:ok,diff --git a/test/pleroma/web/rich_media/parsers/twitter_card_test.exs b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs@@ -17,10 +17,6 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do assert TwitterCard.parse(html, %{}) == %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "site" => nil, "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", "image" => @@ -44,7 +40,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do "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", + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", "image:alt" => "", "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", @@ -61,16 +57,12 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do assert TwitterCard.parse(html, %{}) == %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", "card" => "summary_large_image", "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", "image:alt" => "", - "site" => nil, "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", "url" => @@ -90,13 +82,11 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do assert TwitterCard.parse(html, %{}) == %{ - "site" => "@atlasobscura", "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", "card" => "summary_large_image", "image" => image_path, "description" => "She's the only woman veteran honored with a monument at West Point. But where was she buried?", - "site_name" => "Atlas Obscura", "type" => "article", "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" } @@ -109,12 +99,8 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do assert TwitterCard.parse(html, %{}) == %{ - "site" => nil, "title" => "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", "description" => "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", "image" => @@ -124,4 +110,23 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" } end + + test "takes first image if multiple are specified" do + html = + File.read!("test/fixtures/fulmo.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "description" => "Pri feoj, kiuj devis ordigi falintan arbon.", + "image" => "https://tirifto.xwx.moe/r/ilustrajhoj/pinglordigado.png", + "title" => "Fulmo", + "type" => "website", + "url" => "https://tirifto.xwx.moe/eo/rakontoj/fulmo.html", + "image:alt" => + "Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.", + "image:height" => "630", + "image:width" => "1200" + } + end enddiff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs@@ -558,6 +558,36 @@ defmodule Pleroma.Web.StreamerTest do assert_receive {:render_with_user, _, "status_update.json", ^create, _} refute Streamer.filtered_by_user?(user, edited) end + + test "it streams posts containing followed hashtags on the 'user' stream", %{ + user: user, + token: oauth_token + } do + hashtag = insert(:hashtag, %{name: "tenshi"}) + other_user = insert(:user) + {:ok, user} = User.follow_hashtag(user, hashtag) + + Streamer.get_topic_and_add_socket("user", user, oauth_token) + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey #tenshi"}) + + assert_receive {:render_with_user, _, "update.json", ^activity, _} + end + + test "should not stream private posts containing followed hashtags on the 'user' stream", %{ + user: user, + token: oauth_token + } do + hashtag = insert(:hashtag, %{name: "tenshi"}) + other_user = insert(:user) + {:ok, user} = User.follow_hashtag(user, hashtag) + + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + {:ok, activity} = + CommonAPI.post(other_user, %{status: "hey #tenshi", visibility: "private"}) + + refute_receive {:render_with_user, _, "update.json", ^activity, _} + end end describe "public streams" dodiff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/twitter_api/controller_test.exs@@ -69,7 +69,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> hd() |> Map.keys() - assert keys -- ["id", "app_name", "valid_until"] == [] + assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"]) end test "revoke token", %{token: token} dodiff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs@@ -5,7 +5,6 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do use Pleroma.Web.ConnCase - import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock @@ -34,27 +33,122 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do assert match?(^response_xml, expected_xml) end - test "Webfinger JRD" do + describe "Webfinger" do + test "JRD" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda" + ) + + response = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world") + |> json_response(200) + + assert response["subject"] == "acct:#{user.nickname}@hyrule.world" + + assert response["aliases"] == [ + "https://hyrule.world/users/zelda" + ] + end + + test "XML" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda" + ) + + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> response(200) + + assert response =~ "<Alias>https://hyrule.world/users/zelda</Alias>" + end + end + + test "Webfinger defaults to JSON when no Accept header is provided" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + user = insert(:user, - ap_id: "https://hyrule.world/users/zelda", - also_known_as: ["https://mushroom.kingdom/users/toad"] + ap_id: "https://hyrule.world/users/zelda" ) response = build_conn() - |> put_req_header("accept", "application/jrd+json") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world") |> json_response(200) - assert response["subject"] == "acct:#{user.nickname}@localhost" + assert response["subject"] == "acct:#{user.nickname}@hyrule.world" assert response["aliases"] == [ - "https://hyrule.world/users/zelda", - "https://mushroom.kingdom/users/toad" + "https://hyrule.world/users/zelda" ] end + describe "Webfinger returns also_known_as / aliases in the response" do + test "JSON" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda", + also_known_as: [ + "https://mushroom.kingdom/users/toad", + "https://luigi.mansion/users/kingboo" + ] + ) + + response = + build_conn() + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world") + |> json_response(200) + + assert response["subject"] == "acct:#{user.nickname}@hyrule.world" + + assert response["aliases"] == [ + "https://hyrule.world/users/zelda", + "https://mushroom.kingdom/users/toad", + "https://luigi.mansion/users/kingboo" + ] + end + + test "XML" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda", + also_known_as: [ + "https://mushroom.kingdom/users/toad", + "https://luigi.mansion/users/kingboo" + ] + ) + + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> response(200) + + assert response =~ "<Alias>https://hyrule.world/users/zelda</Alias>" + assert response =~ "<Alias>https://mushroom.kingdom/users/toad</Alias>" + assert response =~ "<Alias>https://luigi.mansion/users/kingboo</Alias>" + end + end + test "reach user on tld, while pleroma is running on subdomain" do clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") @@ -72,17 +166,32 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do assert response["aliases"] == ["https://sub.example.com/users/#{user.nickname}"] 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" + describe "it returns 404 when user isn't found" do + test "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 "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 end - test "Webfinger XML" do + test "Returns JSON when format is not supported" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world") + clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world") + user = insert(:user, ap_id: "https://hyrule.world/users/zelda", @@ -91,34 +200,16 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do response = build_conn() - |> put_req_header("accept", "application/xrd+xml") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - |> response(200) - - assert response =~ "<Alias>https://hyrule.world/users/zelda</Alias>" - assert response =~ "<Alias>https://mushroom.kingdom/users/toad</Alias>" - 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 + |> put_req_header("accept", "text/html") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world") + |> json_response(200) - test "Sends a 404 when invalid format" do - user = insert(:user) + assert response["subject"] == "acct:#{user.nickname}@hyrule.world" - 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" + assert response["aliases"] == [ + "https://hyrule.world/users/zelda", + "https://mushroom.kingdom/users/toad" + ] end test "Sends a 400 when resource param is missing" dodiff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs@@ -39,6 +39,23 @@ defmodule Pleroma.Web.WebFingerTest do end end + test "requires exact match for Endpoint host or WebFinger domain" do + clear_config([Pleroma.Web.WebFinger, :domain], "pleroma.dev") + user = insert(:user) + + assert {:error, "Couldn't find user"} == + WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}xxxx", "JSON") + + assert {:error, "Couldn't find user"} == + WebFinger.webfinger("#{user.nickname}@pleroma.devxxxx", "JSON") + + assert {:ok, _} = + WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "JSON") + + assert {:ok, _} = + WebFinger.webfinger("#{user.nickname}@pleroma.dev", "JSON") + end + describe "fingering" do test "returns error for nonsensical input" do assert {:error, _} = WebFinger.finger("bliblablu")diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs@@ -14,7 +14,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do setup do: clear_config([:email_notifications, :digest]) setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs@@ -11,7 +11,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do alias Pleroma.Workers.Cron.NewUsersDigestWorker setup do - Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok enddiff --git a/test/pleroma/workers/delete_worker_test.exs b/test/pleroma/workers/delete_worker_test.exs@@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.DeleteWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Instances.Instance + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Workers.DeleteWorker + + describe "instance deletion" do + test "creates individual Oban jobs for each user when deleting an instance" do + user1 = insert(:user, nickname: "alice@example.com", name: "Alice") + user2 = insert(:user, nickname: "bob@example.com", name: "Bob") + + {:ok, job} = Instance.delete("example.com") + + assert_enqueued( + worker: DeleteWorker, + args: %{"op" => "delete_instance", "host" => "example.com"} + ) + + {:ok, :ok} = ObanHelpers.perform(job) + + delete_user_jobs = all_enqueued(worker: DeleteWorker, args: %{"op" => "delete_user"}) + + assert length(delete_user_jobs) == 2 + + user_ids = [user1.id, user2.id] + job_user_ids = Enum.map(delete_user_jobs, fn job -> job.args["user_id"] end) + + assert Enum.sort(user_ids) == Enum.sort(job_user_ids) + end + end +enddiff --git a/test/pleroma/workers/poll_worker_test.exs b/test/pleroma/workers/poll_worker_test.exs@@ -11,10 +11,10 @@ defmodule Pleroma.Workers.PollWorkerTest do alias Pleroma.Workers.PollWorker - test "poll notification job" do + test "local poll ending notification job" do user = insert(:user) question = insert(:question, user: user) - activity = insert(:question_activity, question: question) + activity = insert(:question_activity, question: question, user: user) PollWorker.schedule_poll_end(activity) @@ -44,6 +44,65 @@ defmodule Pleroma.Workers.PollWorkerTest do # Ensure notifications were streamed out when job executes assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], :_)) assert called(Pleroma.Web.Push.send(:_)) + + # Skip refreshing polls for local activities + assert activity.local + + refute_enqueued( + worker: PollWorker, + args: %{"op" => "refresh", "activity_id" => activity.id} + ) + end + end + + test "remote poll ending notification job schedules refresh" do + user = insert(:user, local: false) + question = insert(:question, user: user) + activity = insert(:question_activity, question: question, user: user) + + PollWorker.schedule_poll_end(activity) + + expected_job_args = %{"activity_id" => activity.id, "op" => "poll_end"} + + assert_enqueued(args: expected_job_args) + + [job] = all_enqueued(worker: PollWorker) + PollWorker.perform(job) + + refute activity.local + + assert_enqueued( + worker: PollWorker, + args: %{"op" => "refresh", "activity_id" => activity.id} + ) + end + + test "poll refresh" do + user = insert(:user, local: false) + question = insert(:question, user: user) + activity = insert(:question_activity, question: question) + + PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id}) + |> Oban.insert() + + expected_job_args = %{"activity_id" => activity.id, "op" => "refresh"} + + assert_enqueued(args: expected_job_args) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + } + ]) do + [job] = all_enqueued(worker: PollWorker) + PollWorker.perform(job) + + # Ensure updates are streamed out + assert called(Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], :_)) end end enddiff --git a/test/pleroma/workers/publisher_worker_test.exs b/test/pleroma/workers/publisher_worker_test.exs@@ -7,7 +7,9 @@ defmodule Pleroma.Workers.PublisherWorkerTest do use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory + import Mock + alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -37,4 +39,85 @@ defmodule Pleroma.Workers.PublisherWorkerTest do assert {:ok, %Oban.Job{priority: 0}} = Federator.publish(post) end end + + describe "Server reachability:" do + setup do + user = insert(:user) + remote_user = insert(:user, local: false, inbox: "https://example.com/inbox") + {:ok, _, _} = Pleroma.User.follow(remote_user, user) + {:ok, activity} = CommonAPI.post(user, %{status: "Test post"}) + + %{ + user: user, + remote_user: remote_user, + activity: activity + } + end + + test "marks server as unreachable only on final failure", %{activity: activity} do + with_mock Pleroma.Web.Federator, + perform: fn :publish_one, _params -> {:error, :connection_error} end do + # First attempt + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job) + assert Instances.reachable?("https://example.com/inbox") + + # Final attempt + job = %{job | attempt: 5} + assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job) + refute Instances.reachable?("https://example.com/inbox") + end + end + + test "does not mark server as unreachable on successful publish", %{activity: activity} do + with_mock Pleroma.Web.Federator, + perform: fn :publish_one, _params -> {:ok, %{status: 200}} end do + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert :ok = Pleroma.Workers.PublisherWorker.perform(job) + assert Instances.reachable?("https://example.com/inbox") + end + end + + test "cancels job if server is unreachable", %{activity: activity} do + # First mark the server as unreachable + Instances.set_unreachable("https://example.com/inbox") + refute Instances.reachable?("https://example.com/inbox") + + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert {:cancel, :unreachable} = Pleroma.Workers.PublisherWorker.perform(job) + end + end enddiff --git a/test/pleroma/workers/reachability_worker_test.exs b/test/pleroma/workers/reachability_worker_test.exs@@ -0,0 +1,226 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ReachabilityWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Mock + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Workers.ReachabilityWorker + + setup do + ObanHelpers.wipe_all() + :ok + end + + describe "progressive backoff phases" do + test "starts with phase_1min and progresses through phases on failure" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Start with phase_1min + job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1} + } + + # First attempt fails + assert {:error, :timeout} = ReachabilityWorker.perform(job) + + # Should schedule retry for phase_1min (attempt 2) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 2 + + # Clear jobs and simulate second attempt failure + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 2} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule retry for phase_1min (attempt 3) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 3 + + # Clear jobs and simulate third attempt failure (final attempt for phase_1min) + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 3} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule retry for phase_1min (attempt 4) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 4 + + # Clear jobs and simulate fourth attempt failure (final attempt for phase_1min) + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 4} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule next phase (phase_15min) + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 1 + [next_phase_job] = next_phase_jobs + assert next_phase_job.args["phase"] == "phase_15min" + assert next_phase_job.args["attempt"] == 1 + end + end + + test "progresses through all phases correctly" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Simulate all phases failing + phases = ["phase_1min", "phase_15min", "phase_1hour", "phase_8hour", "phase_24hour"] + + Enum.each(phases, fn phase -> + {_interval, max_attempts, next_phase} = get_phase_config(phase) + + # Simulate all attempts failing for this phase + Enum.each(1..max_attempts, fn attempt -> + job = %Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}} + assert {:error, :timeout} = ReachabilityWorker.perform(job) + + if attempt < max_attempts do + # Should schedule retry for same phase + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == phase + assert retry_job.args["attempt"] == attempt + 1 + ObanHelpers.wipe_all() + else + # Should schedule next phase (except for final phase) + if next_phase != "final" do + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 1 + [next_phase_job] = next_phase_jobs + assert next_phase_job.args["phase"] == next_phase + assert next_phase_job.args["attempt"] == 1 + ObanHelpers.wipe_all() + else + # Final phase - no more jobs should be scheduled + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 0 + end + end + end) + end) + end + end + + test "succeeds and stops progression when instance becomes reachable" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:ok, %{status: 200}} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + job = %Oban.Job{args: %{"domain" => domain, "phase" => "phase_1hour", "attempt" => 2}} + + # Should succeed and not schedule any more jobs + assert :ok = ReachabilityWorker.perform(job) + + # Verify set_reachable was called + assert_called(Pleroma.Instances.set_reachable("https://#{domain}")) + + # No more jobs should be scheduled + next_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_jobs) == 0 + end + end + + test "enforces uniqueness per domain using Oban's conflict detection" do + domain = "example.com" + + # Insert first job for the domain + job1 = + %{ + "domain" => domain, + "phase" => "phase_1min", + "attempt" => 1 + } + |> ReachabilityWorker.new() + |> Oban.insert() + + assert {:ok, _} = job1 + + # Try to insert a second job for the same domain with different phase/attempt + job2 = + %{ + "domain" => domain, + "phase" => "phase_15min", + "attempt" => 1 + } + |> ReachabilityWorker.new() + |> Oban.insert() + + # Should fail due to uniqueness constraint (conflict) + assert {:ok, %Oban.Job{conflict?: true}} = job2 + + # Verify only one job exists for this domain + jobs = all_enqueued(worker: ReachabilityWorker) + assert length(jobs) == 1 + [existing_job] = jobs + assert existing_job.args["domain"] == domain + assert existing_job.args["phase"] == "phase_1min" + end + + test "handles new jobs with only domain argument and transitions them to the first phase" do + domain = "legacy.example.com" + + with_mocks([ + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Create a job with only domain (legacy format) + job = %Oban.Job{ + args: %{"domain" => domain} + } + + # Should reschedule with phase_1min and attempt 1 + assert :ok = ReachabilityWorker.perform(job) + + # Check that a new job was scheduled with the correct format + scheduled_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(scheduled_jobs) == 1 + [scheduled_job] = scheduled_jobs + assert scheduled_job.args["domain"] == domain + assert scheduled_job.args["phase"] == "phase_1min" + assert scheduled_job.args["attempt"] == 1 + end + end + end + + defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"} + defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"} + defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"} + defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"} + defp get_phase_config("phase_24hour"), do: {1440, 4, "final"} + defp get_phase_config("final"), do: {nil, 0, nil} +enddiff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs@@ -3,13 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorkerTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true use Oban.Testing, repo: Pleroma.Repo import Mock import Pleroma.Factory alias Pleroma.User + alias Pleroma.Web.CommonAPI alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker @@ -243,4 +244,62 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, _} = ReceiverWorker.perform(oban_job) end + + describe "Server reachability:" do + setup do + user = insert(:user) + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/remote") + {:ok, _, _} = Pleroma.User.follow(user, remote_user) + {:ok, activity} = CommonAPI.post(remote_user, %{status: "Test post"}) + + %{ + user: user, + remote_user: remote_user, + activity: activity + } + end + + test "schedules ReachabilityWorker if host is unreachable", %{activity: activity} do + with_mocks [ + {Pleroma.Web.ActivityPub.Transmogrifier, [], + [handle_incoming: fn _ -> {:ok, activity} end]}, + {Pleroma.Instances, [], [reachable?: fn _ -> false end]}, + {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]} + ] do + job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => activity.data + } + } + + Pleroma.Workers.ReceiverWorker.perform(job) + + assert_enqueued( + worker: Pleroma.Workers.ReachabilityWorker, + args: %{"domain" => "example.com"} + ) + end + end + + test "does not schedule ReachabilityWorker if host is reachable", %{activity: activity} do + with_mocks [ + {Pleroma.Web.ActivityPub.Transmogrifier, [], + [handle_incoming: fn _ -> {:ok, activity} end]}, + {Pleroma.Instances, [], [reachable?: fn _ -> true end]}, + {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]} + ] do + job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => activity.data + } + } + + Pleroma.Workers.ReceiverWorker.perform(job) + + refute_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + end + end + end enddiff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs@@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.RemoteFetcherWorkerTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Workers.RemoteFetcherWorkerdiff --git a/test/support/data_case.ex b/test/support/data_case.ex@@ -117,6 +117,8 @@ defmodule Pleroma.DataCase do Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config) Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Test.StaticConfig) Mox.stub_with(Pleroma.StubbedHTTPSignaturesMock, Pleroma.Test.HTTPSignaturesProxy) + + Mox.stub_with(Pleroma.DateTimeMock, Pleroma.DateTime.Impl) end def ensure_local_uploader(context) dodiff --git a/test/support/factory.ex b/test/support/factory.ex@@ -241,6 +241,7 @@ defmodule Pleroma.Factory do def question_factory(attrs \\ %{}) do user = attrs[:user] || insert(:user) + closed = attrs[:closed] || DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601() data = %{ "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), @@ -251,7 +252,7 @@ defmodule Pleroma.Factory do "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [user.follower_address], "context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(), - "closed" => DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601(), + "closed" => closed, "content" => "Which flavor of ice cream do you prefer?", "oneOf" => [ %{ @@ -509,7 +510,8 @@ defmodule Pleroma.Factory do %Pleroma.Activity{ data: data, actor: data["actor"], - recipients: data["to"] + recipients: data["to"], + local: user.local } |> Map.merge(attrs) end @@ -666,4 +668,11 @@ defmodule Pleroma.Factory do |> Map.merge(params) |> Pleroma.Announcement.add_rendered_properties() end + + def hashtag_factory(params \\ %{}) do + %Pleroma.Hashtag{ + name: "test #{sequence(:hashtag_name, & &1)}" + } + |> Map.merge(params) + end enddiff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex@@ -955,7 +955,7 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}} end - def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do + def get("https://remote.org/users/masto_closed/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -964,7 +964,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do + def get("https://remote.org/users/masto_closed/followers?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -973,7 +973,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:4001/users/masto_closed/following", _, _, _) do + def get("https://remote.org/users/masto_closed/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -982,7 +982,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do + def get("https://remote.org/users/masto_closed/following?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -991,7 +991,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:8080/followers/fuser3", _, _, _) do + def get("https://remote.org/followers/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1000,7 +1000,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:8080/following/fuser3", _, _, _) do + def get("https://remote.org/following/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1009,7 +1009,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:4001/users/fuser2/followers", _, _, _) do + def get("https://remote.org/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1018,7 +1018,7 @@ defmodule HttpRequestMock do }} end - def get("http://localhost:4001/users/fuser2/following", _, _, _) do + def get("https://remote.org/users/fuser2/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1494,6 +1494,11 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}} end + def get("https://instagram.com/longtext", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/instagram_longtext.html")}} + end + def get("https://example.com/non-ogp", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}} @@ -1701,6 +1706,24 @@ defmodule HttpRequestMock do }} end + def post("https://api-free.deepl.com/v2/translate" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/deepl-translation.json"), + headers: [{"content-type", "application/json"}] + }} + end + + def post("https://api-free.deepl.com/v2/languages" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/deepl-languages-list.json"), + headers: [{"content-type", "application/json"}] + }} + end + def post(url, query, body, headers) do {:error, "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} @@ -1720,7 +1743,8 @@ defmodule HttpRequestMock do "https://example.com/twitter-card", "https://google.com/", "https://pleroma.local/notice/9kCP7V", - "https://yahoo.com/" + "https://yahoo.com/", + "https://instagram.com/longtext" ] def head(url, _query, _body, _headers) when url in @rich_media_mocks dodiff --git a/test/support/mocks.ex b/test/support/mocks.ex@@ -33,3 +33,10 @@ Mox.defmock(Pleroma.StubbedHTTPSignaturesMock, for: Pleroma.HTTPSignaturesAPI) Mox.defmock(Pleroma.LoggerMock, for: Pleroma.Logging) Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) + +Mox.defmock(Pleroma.Language.LanguageDetectorMock, + for: Pleroma.Language.LanguageDetector.Provider +) + +Mox.defmock(Pleroma.DateTimeMock, for: Pleroma.DateTime) +Mox.defmock(Pleroma.MogrifyMock, for: Pleroma.MogrifyBehaviour)diff --git a/test/support/translation_mock.ex b/test/support/translation_mock.ex@@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule TranslationMock do + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "TranslationMock" + + @impl Provider + def configured?, do: true + + @impl Provider + def translate(content, source_language, _target_language) do + {:ok, + %{ + content: content |> String.reverse(), + detected_source_language: source_language, + provider: @name + }} + end + + @impl Provider + def supported_languages(_) do + {:ok, ["en", "pl"]} + end + + @impl Provider + def languages_matrix do + {:ok, + %{ + "en" => ["pl"], + "pl" => ["en"] + }} + end + + @impl Provider + def name, do: @name +enddiff --git a/test/test_helper.exs b/test/test_helper.exs@@ -2,8 +2,6 @@ # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -Code.put_compiler_option(:warnings_as_errors, true) - ExUnit.configure(capture_log: true, max_cases: System.schedulers_online()) ExUnit.start(exclude: [:federated]) @@ -34,7 +32,13 @@ defmodule Pleroma.Test.StaticConfig do @behaviour Pleroma.Config.Getting @config Application.get_all_env(:pleroma) + @impl true def get(path, default \\ nil) do get_in(@config, path) || default end + + @impl true + def get!(path) do + get_in(@config, path) + end end