Resolve merge conflicts
authorrinpatch <rinpatch@sdf.org>
Sat, 20 Jul 2019 19:04:47 +0000 (22:04 +0300)
committerrinpatch <rinpatch@sdf.org>
Sat, 20 Jul 2019 19:04:47 +0000 (22:04 +0300)
114 files changed:
CC-BY-NC-ND-4.0 [deleted file]
CHANGELOG.md
config/config.exs
config/test.exs
docs/api/differences_in_mastoapi_responses.md
docs/clients.md
docs/config.md
docs/config/howto_mediaproxy.md
docs/config/howto_set_richmedia_cache_ttl_based_on_image.md [new file with mode: 0644]
docs/installation/alpine_linux_en.md
docs/installation/arch_linux_en.md
docs/installation/centos7_en.md
docs/installation/debian_based_en.md
docs/installation/debian_based_jp.md
docs/installation/gentoo_en.md
docs/installation/otp_en.md
lib/mix/tasks/pleroma/config.ex
lib/mix/tasks/pleroma/user.ex
lib/pleroma/application.ex
lib/pleroma/config/transfer_task.ex
lib/pleroma/http/connection.ex
lib/pleroma/http/http.ex
lib/pleroma/keys.ex
lib/pleroma/list.ex
lib/pleroma/notification.ex
lib/pleroma/object/containment.ex
lib/pleroma/object/fetcher.ex
lib/pleroma/plugs/authentication_plug.ex
lib/pleroma/plugs/http_signature.ex
lib/pleroma/plugs/mapped_signature_to_identity_plug.ex [new file with mode: 0644]
lib/pleroma/plugs/rate_limiter.ex
lib/pleroma/reverse_proxy/reverse_proxy.ex
lib/pleroma/signature.ex
lib/pleroma/upload/filter/dedupe.ex
lib/pleroma/upload/filter/mogrifun.ex
lib/pleroma/upload/filter/mogrify.ex
lib/pleroma/uploaders/uploader.ex
lib/pleroma/user.ex
lib/pleroma/user/info.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/internal_fetch_actor.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/mrf/mention_policy.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/publisher.ex
lib/pleroma/web/activity_pub/relay.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/activity_pub/views/user_view.ex
lib/pleroma/web/activity_pub/visibility.ex
lib/pleroma/web/auth/pleroma_authenticator.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/mastodon_api/mastodon_api.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/media_proxy/media_proxy.ex
lib/pleroma/web/media_proxy/media_proxy_controller.ex [moved from lib/pleroma/web/media_proxy/controller.ex with 71% similarity]
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/rich_media/parser.ex
lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex [new file with mode: 0644]
lib/pleroma/web/rich_media/parsers/ttl/ttl.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/salmon/salmon.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
lib/pleroma/web/uploader_controller.ex
lib/pleroma/web/web_finger/web_finger.ex
mix.exs
mix.lock
priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs [new file with mode: 0644]
priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs [new file with mode: 0644]
priv/static/schemas/litepub-0.1.jsonld
test/fixtures/rich_media/amz.html [new file with mode: 0644]
test/list_test.exs
test/notification_test.exs
test/object/containment_test.exs
test/object/fetcher_test.exs
test/plugs/authentication_plug_test.exs
test/plugs/http_signature_plug_test.exs
test/plugs/mapped_identity_to_signature_plug_test.exs [new file with mode: 0644]
test/plugs/rate_limiter_test.exs
test/reverse_proxy_test.exs
test/signature_test.exs [new file with mode: 0644]
test/support/data_case.ex
test/support/http_request_mock.ex
test/tasks/config_test.exs
test/upload/filter/dedupe_test.exs [new file with mode: 0644]
test/upload/filter/mogrifun_test.exs [new file with mode: 0644]
test/upload/filter/mogrify_test.exs [new file with mode: 0644]
test/upload/filter_test.exs [new file with mode: 0644]
test/upload_test.exs
test/user_test.exs
test/web/activity_pub/activity_pub_controller_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/mrf/mention_policy_test.exs [new file with mode: 0644]
test/web/activity_pub/transmogrifier_test.exs
test/web/activity_pub/visibilty_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/common_api/common_api_test.exs
test/web/common_api/common_api_utils_test.exs
test/web/mastodon_api/account_view_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/mastodon_api/search_controller_test.exs
test/web/mastodon_api/status_view_test.exs
test/web/media_proxy/media_proxy_controller_test.exs [new file with mode: 0644]
test/web/media_proxy/media_proxy_test.exs [moved from test/media_proxy_test.exs with 87% similarity]
test/web/node_info_test.exs
test/web/ostatus/ostatus_controller_test.exs
test/web/rich_media/aws_signed_url_test.exs [new file with mode: 0644]
test/web/twitter_api/twitter_api_controller_test.exs
test/web/twitter_api/util_controller_test.exs
test/web/uploader_controller_test.exs [new file with mode: 0644]

diff --git a/CC-BY-NC-ND-4.0 b/CC-BY-NC-ND-4.0
deleted file mode 100644 (file)
index 4865442..0000000
+++ /dev/null
@@ -1,403 +0,0 @@
-Attribution-NonCommercial-NoDerivatives 4.0 International
-
-=======================================================================
-
-Creative Commons Corporation ("Creative Commons") is not a law firm and
-does not provide legal services or legal advice. Distribution of
-Creative Commons public licenses does not create a lawyer-client or
-other relationship. Creative Commons makes its licenses and related
-information available on an "as-is" basis. Creative Commons gives no
-warranties regarding its licenses, any material licensed under their
-terms and conditions, or any related information. Creative Commons
-disclaims all liability for damages resulting from their use to the
-fullest extent possible.
-
-Using Creative Commons Public Licenses
-
-Creative Commons public licenses provide a standard set of terms and
-conditions that creators and other rights holders may use to share
-original works of authorship and other material subject to copyright
-and certain other rights specified in the public license below. The
-following considerations are for informational purposes only, are not
-exhaustive, and do not form part of our licenses.
-
-     Considerations for licensors: Our public licenses are
-     intended for use by those authorized to give the public
-     permission to use material in ways otherwise restricted by
-     copyright and certain other rights. Our licenses are
-     irrevocable. Licensors should read and understand the terms
-     and conditions of the license they choose before applying it.
-     Licensors should also secure all rights necessary before
-     applying our licenses so that the public can reuse the
-     material as expected. Licensors should clearly mark any
-     material not subject to the license. This includes other CC-
-     licensed material, or material used under an exception or
-     limitation to copyright. More considerations for licensors:
-       wiki.creativecommons.org/Considerations_for_licensors
-
-     Considerations for the public: By using one of our public
-     licenses, a licensor grants the public permission to use the
-     licensed material under specified terms and conditions. If
-     the licensor's permission is not necessary for any reason--for
-     example, because of any applicable exception or limitation to
-     copyright--then that use is not regulated by the license. Our
-     licenses grant only permissions under copyright and certain
-     other rights that a licensor has authority to grant. Use of
-     the licensed material may still be restricted for other
-     reasons, including because others have copyright or other
-     rights in the material. A licensor may make special requests,
-     such as asking that all changes be marked or described.
-     Although not required by our licenses, you are encouraged to
-     respect those requests where reasonable. More considerations
-     for the public: 
-       wiki.creativecommons.org/Considerations_for_licensees
-
-=======================================================================
-
-Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
-International Public License
-
-By exercising the Licensed Rights (defined below), You accept and agree
-to be bound by the terms and conditions of this Creative Commons
-Attribution-NonCommercial-NoDerivatives 4.0 International Public
-License ("Public License"). To the extent this Public License may be
-interpreted as a contract, You are granted the Licensed Rights in
-consideration of Your acceptance of these terms and conditions, and the
-Licensor grants You such rights in consideration of benefits the
-Licensor receives from making the Licensed Material available under
-these terms and conditions.
-
-
-Section 1 -- Definitions.
-
-  a. Adapted Material means material subject to Copyright and Similar
-     Rights that is derived from or based upon the Licensed Material
-     and in which the Licensed Material is translated, altered,
-     arranged, transformed, or otherwise modified in a manner requiring
-     permission under the Copyright and Similar Rights held by the
-     Licensor. For purposes of this Public License, where the Licensed
-     Material is a musical work, performance, or sound recording,
-     Adapted Material is always produced where the Licensed Material is
-     synched in timed relation with a moving image.
-
-  b. Copyright and Similar Rights means copyright and/or similar rights
-     closely related to copyright including, without limitation,
-     performance, broadcast, sound recording, and Sui Generis Database
-     Rights, without regard to how the rights are labeled or
-     categorized. For purposes of this Public License, the rights
-     specified in Section 2(b)(1)-(2) are not Copyright and Similar
-     Rights.
-
-  c. Effective Technological Measures means those measures that, in the
-     absence of proper authority, may not be circumvented under laws
-     fulfilling obligations under Article 11 of the WIPO Copyright
-     Treaty adopted on December 20, 1996, and/or similar international
-     agreements.
-
-  d. Exceptions and Limitations means fair use, fair dealing, and/or
-     any other exception or limitation to Copyright and Similar Rights
-     that applies to Your use of the Licensed Material.
-
-  e. Licensed Material means the artistic or literary work, database,
-     or other material to which the Licensor applied this Public
-     License.
-
-  f. Licensed Rights means the rights granted to You subject to the
-     terms and conditions of this Public License, which are limited to
-     all Copyright and Similar Rights that apply to Your use of the
-     Licensed Material and that the Licensor has authority to license.
-
-  g. Licensor means the individual(s) or entity(ies) granting rights
-     under this Public License.
-
-  h. NonCommercial means not primarily intended for or directed towards
-     commercial advantage or monetary compensation. For purposes of
-     this Public License, the exchange of the Licensed Material for
-     other material subject to Copyright and Similar Rights by digital
-     file-sharing or similar means is NonCommercial provided there is
-     no payment of monetary compensation in connection with the
-     exchange.
-
-  i. Share means to provide material to the public by any means or
-     process that requires permission under the Licensed Rights, such
-     as reproduction, public display, public performance, distribution,
-     dissemination, communication, or importation, and to make material
-     available to the public including in ways that members of the
-     public may access the material from a place and at a time
-     individually chosen by them.
-
-  j. Sui Generis Database Rights means rights other than copyright
-     resulting from Directive 96/9/EC of the European Parliament and of
-     the Council of 11 March 1996 on the legal protection of databases,
-     as amended and/or succeeded, as well as other essentially
-     equivalent rights anywhere in the world.
-
-  k. You means the individual or entity exercising the Licensed Rights
-     under this Public License. Your has a corresponding meaning.
-
-
-Section 2 -- Scope.
-
-  a. License grant.
-
-       1. Subject to the terms and conditions of this Public License,
-          the Licensor hereby grants You a worldwide, royalty-free,
-          non-sublicensable, non-exclusive, irrevocable license to
-          exercise the Licensed Rights in the Licensed Material to:
-
-            a. reproduce and Share the Licensed Material, in whole or
-               in part, for NonCommercial purposes only; and
-
-            b. produce and reproduce, but not Share, Adapted Material
-               for NonCommercial purposes only.
-
-       2. Exceptions and Limitations. For the avoidance of doubt, where
-          Exceptions and Limitations apply to Your use, this Public
-          License does not apply, and You do not need to comply with
-          its terms and conditions.
-
-       3. Term. The term of this Public License is specified in Section
-          6(a).
-
-       4. Media and formats; technical modifications allowed. The
-          Licensor authorizes You to exercise the Licensed Rights in
-          all media and formats whether now known or hereafter created,
-          and to make technical modifications necessary to do so. The
-          Licensor waives and/or agrees not to assert any right or
-          authority to forbid You from making technical modifications
-          necessary to exercise the Licensed Rights, including
-          technical modifications necessary to circumvent Effective
-          Technological Measures. For purposes of this Public License,
-          simply making modifications authorized by this Section 2(a)
-          (4) never produces Adapted Material.
-
-       5. Downstream recipients.
-
-            a. Offer from the Licensor -- Licensed Material. Every
-               recipient of the Licensed Material automatically
-               receives an offer from the Licensor to exercise the
-               Licensed Rights under the terms and conditions of this
-               Public License.
-
-            b. No downstream restrictions. You may not offer or impose
-               any additional or different terms or conditions on, or
-               apply any Effective Technological Measures to, the
-               Licensed Material if doing so restricts exercise of the
-               Licensed Rights by any recipient of the Licensed
-               Material.
-
-       6. No endorsement. Nothing in this Public License constitutes or
-          may be construed as permission to assert or imply that You
-          are, or that Your use of the Licensed Material is, connected
-          with, or sponsored, endorsed, or granted official status by,
-          the Licensor or others designated to receive attribution as
-          provided in Section 3(a)(1)(A)(i).
-
-  b. Other rights.
-
-       1. Moral rights, such as the right of integrity, are not
-          licensed under this Public License, nor are publicity,
-          privacy, and/or other similar personality rights; however, to
-          the extent possible, the Licensor waives and/or agrees not to
-          assert any such rights held by the Licensor to the limited
-          extent necessary to allow You to exercise the Licensed
-          Rights, but not otherwise.
-
-       2. Patent and trademark rights are not licensed under this
-          Public License.
-
-       3. To the extent possible, the Licensor waives any right to
-          collect royalties from You for the exercise of the Licensed
-          Rights, whether directly or through a collecting society
-          under any voluntary or waivable statutory or compulsory
-          licensing scheme. In all other cases the Licensor expressly
-          reserves any right to collect such royalties, including when
-          the Licensed Material is used other than for NonCommercial
-          purposes.
-
-
-Section 3 -- License Conditions.
-
-Your exercise of the Licensed Rights is expressly made subject to the
-following conditions.
-
-  a. Attribution.
-
-       1. If You Share the Licensed Material, You must:
-
-            a. retain the following if it is supplied by the Licensor
-               with the Licensed Material:
-
-                 i. identification of the creator(s) of the Licensed
-                    Material and any others designated to receive
-                    attribution, in any reasonable manner requested by
-                    the Licensor (including by pseudonym if
-                    designated);
-
-                ii. a copyright notice;
-
-               iii. a notice that refers to this Public License;
-
-                iv. a notice that refers to the disclaimer of
-                    warranties;
-
-                 v. a URI or hyperlink to the Licensed Material to the
-                    extent reasonably practicable;
-
-            b. indicate if You modified the Licensed Material and
-               retain an indication of any previous modifications; and
-
-            c. indicate the Licensed Material is licensed under this
-               Public License, and include the text of, or the URI or
-               hyperlink to, this Public License.
-
-          For the avoidance of doubt, You do not have permission under
-          this Public License to Share Adapted Material.
-
-       2. You may satisfy the conditions in Section 3(a)(1) in any
-          reasonable manner based on the medium, means, and context in
-          which You Share the Licensed Material. For example, it may be
-          reasonable to satisfy the conditions by providing a URI or
-          hyperlink to a resource that includes the required
-          information.
-
-       3. If requested by the Licensor, You must remove any of the
-          information required by Section 3(a)(1)(A) to the extent
-          reasonably practicable.
-
-
-Section 4 -- Sui Generis Database Rights.
-
-Where the Licensed Rights include Sui Generis Database Rights that
-apply to Your use of the Licensed Material:
-
-  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
-     to extract, reuse, reproduce, and Share all or a substantial
-     portion of the contents of the database for NonCommercial purposes
-     only and provided You do not Share Adapted Material;
-
-  b. if You include all or a substantial portion of the database
-     contents in a database in which You have Sui Generis Database
-     Rights, then the database in which You have Sui Generis Database
-     Rights (but not its individual contents) is Adapted Material; and
-
-  c. You must comply with the conditions in Section 3(a) if You Share
-     all or a substantial portion of the contents of the database.
-
-For the avoidance of doubt, this Section 4 supplements and does not
-replace Your obligations under this Public License where the Licensed
-Rights include other Copyright and Similar Rights.
-
-
-Section 5 -- Disclaimer of Warranties and Limitation of Liability.
-
-  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
-     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
-     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
-     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
-     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
-     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
-     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
-     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
-     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
-     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
-
-  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
-     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
-     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
-     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
-     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
-     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
-     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
-     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
-     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
-
-  c. The disclaimer of warranties and limitation of liability provided
-     above shall be interpreted in a manner that, to the extent
-     possible, most closely approximates an absolute disclaimer and
-     waiver of all liability.
-
-
-Section 6 -- Term and Termination.
-
-  a. This Public License applies for the term of the Copyright and
-     Similar Rights licensed here. However, if You fail to comply with
-     this Public License, then Your rights under this Public License
-     terminate automatically.
-
-  b. Where Your right to use the Licensed Material has terminated under
-     Section 6(a), it reinstates:
-
-       1. automatically as of the date the violation is cured, provided
-          it is cured within 30 days of Your discovery of the
-          violation; or
-
-       2. upon express reinstatement by the Licensor.
-
-     For the avoidance of doubt, this Section 6(b) does not affect any
-     right the Licensor may have to seek remedies for Your violations
-     of this Public License.
-
-  c. For the avoidance of doubt, the Licensor may also offer the
-     Licensed Material under separate terms or conditions or stop
-     distributing the Licensed Material at any time; however, doing so
-     will not terminate this Public License.
-
-  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
-     License.
-
-
-Section 7 -- Other Terms and Conditions.
-
-  a. The Licensor shall not be bound by any additional or different
-     terms or conditions communicated by You unless expressly agreed.
-
-  b. Any arrangements, understandings, or agreements regarding the
-     Licensed Material not stated herein are separate from and
-     independent of the terms and conditions of this Public License.
-
-
-Section 8 -- Interpretation.
-
-  a. For the avoidance of doubt, this Public License does not, and
-     shall not be interpreted to, reduce, limit, restrict, or impose
-     conditions on any use of the Licensed Material that could lawfully
-     be made without permission under this Public License.
-
-  b. To the extent possible, if any provision of this Public License is
-     deemed unenforceable, it shall be automatically reformed to the
-     minimum extent necessary to make it enforceable. If the provision
-     cannot be reformed, it shall be severed from this Public License
-     without affecting the enforceability of the remaining terms and
-     conditions.
-
-  c. No term or condition of this Public License will be waived and no
-     failure to comply consented to unless expressly agreed to by the
-     Licensor.
-
-  d. Nothing in this Public License constitutes or may be interpreted
-     as a limitation upon, or waiver of, any privileges and immunities
-     that apply to the Licensor or You, including from the legal
-     processes of any jurisdiction or authority.
-
-=======================================================================
-
-Creative Commons is not a party to its public
-licenses. Notwithstanding, Creative Commons may elect to apply one of
-its public licenses to material it publishes and in those instances
-will be considered the “Licensor.” The text of the Creative Commons
-public licenses is dedicated to the public domain under the CC0 Public
-Domain Dedication. Except for the limited purpose of indicating that
-material is shared under a Creative Commons public license or as
-otherwise permitted by the Creative Commons policies published at
-creativecommons.org/policies, Creative Commons does not authorize the
-use of the trademark "Creative Commons" or any other trademark or logo
-of Creative Commons without its prior written consent including,
-without limitation, in connection with any unauthorized modifications
-to any of its public licenses or any other arrangements,
-understandings, or agreements concerning use of licensed material. For
-the avoidance of doubt, this paragraph does not form part of the
-public licenses.
-
-Creative Commons may be contacted at creativecommons.org.
-
index 405b7680c860d3489eebc1007478d8e579d837a1..f60f3ed978d576d12a2c8e0d75b764d4569e7543 100644 (file)
@@ -10,32 +10,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
 - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
 - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
+- Mastodon API: Unsubscribe followers when they unfollow a user
 
 ### Fixed
 - Not being able to pin unlisted posts
 - Metadata rendering errors resulting in the entire page being inaccessible
+- Federation/MediaProxy not working with instances that have wrong certificate order
 - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
 - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
 - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
 - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
+- Existing user id not being preserved on insert conflict
 
 ### Added
 - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
-Configuration: `federation_incoming_replies_max_depth` option
+- MRF: Support for excluding specific domains from Transparency.
+- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
+- Configuration: `federation_incoming_replies_max_depth` option
 - Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
 - Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
 - Mastodon API, extension: Ability to reset avatar, profile banner, and background
+- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
+- Mastodon API: Add support for muting/unmuting notifications
+- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
+- Mastodon API: Add `pleroma.deactivated` to the Account entity
+- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
+- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
 - Admin API: Return users' tags when querying reports
 - Admin API: Return avatar and display name when querying users
 - Admin API: Allow querying user by ID
 - Admin API: Added support for `tuples`.
 - Added synchronization of following/followers counters for external users
 - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
-- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
+- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
+- Addressable lists
+- Twitter API: added rate limit for `/api/account/password_reset` endpoint.
+- ActivityPub: Add an internal service actor for fetching ActivityPub objects.
+- ActivityPub: Optional signing of ActivityPub object fetches.
 
 ### Changed
 - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
 - Admin API: changed json structure for saving config settings.
+- RichMedia: parsers and their order are configured in `rich_media` config.
+- RichMedia: add the rich media ttl based on image expiration time.
+
+## [1.0.1] - 2019-07-14
+### Security
+- OStatus: fix an object spoofing vulnerability.
 
 ## [1.0.0] - 2019-06-29
 ### Security
index 99b500993c7284c72723a5e284f1e12374bbc060..5694118668942c3c3ac4686e5a998e028b21957e 100644 (file)
@@ -194,6 +194,8 @@ config :pleroma, :http,
   send_user_agent: true,
   adapter: [
     ssl_options: [
+      # Workaround for remote server certificate chain issues
+      partial_chain: &:hackney_connect.partial_chain/1,
       # We don't support TLS v1.3 yet
       versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
     ]
@@ -238,6 +240,7 @@ config :pleroma, :instance,
     "text/bbcode"
   ],
   mrf_transparency: true,
+  mrf_transparency_exclusions: [],
   autofollowed_nicknames: [],
   max_pinned_statuses: 1,
   no_attachment_links: false,
@@ -302,7 +305,8 @@ config :pleroma, :activitypub,
   accept_blocks: true,
   unfollow_blocked: true,
   outgoing_blocks: true,
-  follow_handshake_timeout: 500
+  follow_handshake_timeout: 500,
+  sign_object_fetches: true
 
 config :pleroma, :user, deny_follow_blocked: true
 
@@ -336,7 +340,13 @@ config :pleroma, :mrf_subchain, match_actor: %{}
 config :pleroma, :rich_media,
   enabled: true,
   ignore_hosts: [],
-  ignore_tld: ["local", "localdomain", "lan"]
+  ignore_tld: ["local", "localdomain", "lan"],
+  parsers: [
+    Pleroma.Web.RichMedia.Parsers.TwitterCard,
+    Pleroma.Web.RichMedia.Parsers.OGP,
+    Pleroma.Web.RichMedia.Parsers.OEmbed
+  ],
+  ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
 
 config :pleroma, :media_proxy,
   enabled: false,
@@ -519,7 +529,12 @@ config :http_signatures,
 
 config :pleroma, :rate_limit,
   search: [{1000, 10}, {1000, 30}],
-  app_account_creation: {1_800_000, 25}
+  app_account_creation: {1_800_000, 25},
+  relations_actions: {10_000, 10},
+  relation_id_action: {60_000, 2},
+  statuses_actions: {10_000, 15},
+  status_id_action: {60_000, 3},
+  password_reset: {1_800_000, 5}
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
index 28eea3b0074d4a6593fc719199bf31955bb7c706..aded8600d169b580d47ad9a571bba923f0dac50d 100644 (file)
@@ -32,6 +32,8 @@ config :pleroma, :instance,
   federating: false,
   external_user_synchronization: false
 
+config :pleroma, :activitypub, sign_object_fetches: false
+
 # Configure your database
 config :pleroma, Pleroma.Repo,
   adapter: Ecto.Adapters.Postgres,
@@ -68,7 +70,8 @@ config :pleroma, Pleroma.ScheduledActivity,
 
 config :pleroma, :rate_limit,
   search: [{1000, 30}, {1000, 30}],
-  app_account_creation: {10_000, 5}
+  app_account_creation: {10_000, 5},
+  password_reset: {1000, 30}
 
 config :pleroma, :http_security, report_uri: "https://endpoint.com"
 
index 3ee7115cf5b696c58848eb45dc1989ce0498b3b2..1907d70c809ec41fa4e36abf9549de054956cb8c 100644 (file)
@@ -16,9 +16,11 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
 
 ## Statuses
 
+- `visibility`: has an additional possible value `list`
+
 Has these additional fields under the `pleroma` object:
 
-- `local`: true if the post was made on the local instance.
+- `local`: true if the post was made on the local instance
 - `conversation_id`: the ID of the conversation the status is associated with (if any)
 - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
 - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
@@ -32,7 +34,10 @@ Has these additional fields under the `pleroma` object:
 
 ## Accounts
 
-- `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc.
+The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
+
+- `/api/v1/accounts/:id`
+- `/api/v1/accounts/:id/statuses`
 
 Has these additional fields under the `pleroma` object:
 
@@ -45,6 +50,7 @@ Has these additional fields under the `pleroma` object:
 - `hide_follows`: boolean, true when the user has follow hiding enabled
 - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
 - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
+- `deactivated`: boolean, true when the user is deactivated
 
 ### Source
 
@@ -72,6 +78,7 @@ Additional parameters can be added to the JSON body/Form data:
 - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
 - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
 - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
+- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
 
 ## PATCH `/api/v1/update_credentials`
 
index 30358c210c02b368d808793ec0cc7643eeb6b378..9029361f8c95741d76ecfa2202a2868d17d556f9 100644 (file)
@@ -31,10 +31,11 @@ Feel free to contact us to be added to this list!
 - Features: No Streaming
 
 ### Fedilab
-- Source Code: <https://gitlab.com/tom79/mastalab/>
-- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79)
+- Homepage: <https://fedilab.app/>
+- Source Code: <https://framagit.org/tom79/fedilab/>
+- Contact: [@fedilab@framapiaf.org](https://framapiaf.org/users/fedilab)
 - Platforms: Android
-- Features: Streaming Ready
+- Features: Streaming Ready, Moderation, Text Formatting 
 
 ### Nekonium
 - Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
index 140789d877ffa54b7fc88e312b6cbab31cc57816..02f86dc169abfb6f9be9215c9d7a949b885e40de 100644 (file)
@@ -101,11 +101,13 @@ config :pleroma, Pleroma.Emails.Mailer,
   * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
   * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
   * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
+  * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (see `:mrf_mention` section)
 * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
 * `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
 * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
 * `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
+* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency.  The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
 * `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default.
 * `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values:
   * "email": Copy and preprend re:, as in email.
@@ -270,6 +272,9 @@ config :pleroma, :mrf_subchain,
 * `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
 * `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
 
+## :mrf_mention
+* `actors`: A list of actors, for which to drop any posts mentioning.
+
 ## :media_proxy
 * `enabled`: Enables proxying of remote media to the instance’s proxy
 * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
@@ -327,6 +332,7 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start
 * ``unfollow_blocked``: Whether blocks result in people getting unfollowed
 * ``outgoing_blocks``: Whether to federate blocks to other instances
 * ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
+* ``sign_object_fetches``: Sign object fetches with HTTP signatures
 
 ## :http_security
 * ``enabled``: Whether the managed content security policy is enabled
@@ -424,6 +430,7 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`.
 * `enabled`: if enabled the instance will parse metadata from attached links to generate link previews
 * `ignore_hosts`: list of hosts which will be ignored by the metadata parser. For example `["accounts.google.com", "xss.website"]`, defaults to `[]`.
 * `ignore_tld`: list TLDs (top-level domains) which will ignore for parse metadata. default is ["local", "localdomain", "lan"]
+* `parsers`: list of Rich Media parsers
 
 ## :fetch_initial_posts
 * `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
@@ -640,3 +647,12 @@ A keyword list of rate limiters where a key is a limiter name and value is the l
 It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
 
 See [`Pleroma.Plugs.RateLimiter`](Pleroma.Plugs.RateLimiter.html) documentation for examples.
+
+Supported rate limiters:
+
+* `:search` for the search requests (account & status search etc.)
+* `:app_account_creation` for registering user accounts from the same IP address
+* `:relations_actions` for actions on relations with all users (follow, unfollow)
+* `:relation_id_action` for actions on relation with a specific user (follow, unfollow)
+* `:statuses_actions` for create / delete / fav / unfav / reblog / unreblog actions on any statuses
+* `:status_id_action` for fav / unfav or reblog / unreblog actions on the same status by the same user
index fb731112b8919c9ec873671b7d8c2ade4b66fa7b..ed70c3ed484339684939d9c940eee1472dec79be 100644 (file)
@@ -24,7 +24,9 @@ If you came here from one of the installation guides, take a look at the example
 ```
 config :pleroma, :media_proxy,
       enabled: true,
-      redirect_on_failure: true
+      proxy_opts: [
+            redirect_on_failure: true
+      ]
       #base_url: "https://cache.pleroma.social"
 ```
 If you want to use a subdomain to serve the files, uncomment `base_url`, change the url and add a comma after `true` in the previous line.
diff --git a/docs/config/howto_set_richmedia_cache_ttl_based_on_image.md b/docs/config/howto_set_richmedia_cache_ttl_based_on_image.md
new file mode 100644 (file)
index 0000000..bfee5a9
--- /dev/null
@@ -0,0 +1,33 @@
+# How to set rich media cache ttl based on image ttl
+## Explanation
+
+Richmedia are cached without the ttl but the rich media may have image which can expire, like aws signed url.
+In such cases the old image url (expired) is returned from the media cache.
+
+So to avoid such situation we can define a module that will set ttl based on image.
+The module must adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
+
+### Example
+
+```exs
+defmodule MyModule do
+  @behaviour Pleroma.Web.RichMedia.Parser.TTL
+
+  @impl Pleroma.Web.RichMedia.Parser.TTL
+  def ttl(data, url) do
+    image_url = Map.get(data, :image)
+    # do some parsing in the url and get the ttl of the image
+    # return ttl is unix time
+    parse_ttl_from_url(image_url)
+  end
+end
+```
+
+And update the config
+
+```exs
+config :pleroma, :rich_media,
+  ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl, MyModule]
+```
+
+> For reference there is a parser for AWS signed URL `Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl`, it's enabled by default.
index a9b5afd33e93d1573767be218967801de58b6fca..1f300f353403d78d2b0891cc93ee2fa1956a498a 100644 (file)
@@ -202,7 +202,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
 
 #### Further reading
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index d25fe1d12101585a0769e8d56fb42139295f2b3a..fd8b5d10794531d5296ad47bf0e73067edf3f429 100644 (file)
@@ -200,7 +200,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
 
 #### Further reading
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index 9ae55cc4a505b25482b59134b084aa5a2510d4cb..729fcab72d32160761ec45937a5a113c6edc0462 100644 (file)
@@ -264,7 +264,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
 
 #### Further reading
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index 08f8bf3697a881294f03c6dcbf6212a9549f20cd..46165e2c146b47718a3fe76fae0426b6e149036a 100644 (file)
@@ -190,7 +190,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
 
 #### Further reading
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index 627ddfe396f18654d7309a2e887ad4e0a3dd6253..caf72363b6398573801c8563b428fe3ad0ea52d0 100644 (file)
@@ -180,7 +180,6 @@ mix set_moderator username [true|false]
 
 #### コンフィギュレーションとカスタマイズ
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index 1a9023983225b52f16bdb2bb739c4c34a1044fe5..5b62344b1c716340d96910607719ff9858adaa9f 100644 (file)
@@ -283,7 +283,6 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
 
 #### Further reading
 
-* [Admin tasks](Admin tasks)
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
 * [Hardening your instance](hardening.html)
index 9b851e39562c7ef7b755f9fddc2d6c86a69d6534..5b50e1838723c7587939ced550f7656e7329a256 100644 (file)
@@ -242,6 +242,14 @@ So for example, if the task is `mix pleroma.user set admin --admin`, you should
 ```sh
 su pleroma -s $SHELL -lc "./bin/pleroma_ctl user set admin --admin"
 ```
+
+## Create your first user and set as admin
+```sh
+cd /opt/pleroma/bin
+su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin"
+```
+This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password.
+
 ### Updating
 Generally, doing the following is enough:
 ```sh
index a71bcd447bb648a4e9b9649273811910f4a1459e..a7d0fac5da97d51b4c4015224af156595d49cd84 100644 (file)
@@ -28,6 +28,14 @@ defmodule Mix.Tasks.Pleroma.Config do
       |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
       |> Enum.each(fn {k, v} ->
         key = to_string(k) |> String.replace("Elixir.", "")
+
+        key =
+          if String.starts_with?(key, "Pleroma.") do
+            key
+          else
+            ":" <> key
+          end
+
         {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v})
         Mix.shell().info("#{key} is migrated.")
       end)
@@ -53,17 +61,9 @@ defmodule Mix.Tasks.Pleroma.Config do
 
       Repo.all(Config)
       |> Enum.each(fn config ->
-        mark =
-          if String.starts_with?(config.key, "Pleroma.") or
-               String.starts_with?(config.key, "Ueberauth"),
-             do: ",",
-             else: ":"
-
         IO.write(
           file,
-          "config :#{config.group}, #{config.key}#{mark} #{
-            inspect(Config.from_binary(config.value))
-          }\r\n"
+          "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n"
         )
 
         if delete? do
index 8a78b4fe663b7a8e792f773716f9b2fb6b4a6caa..c9b84b8f90eec967e399e37cf0b4afb87ecdf4e5 100644 (file)
@@ -62,6 +62,10 @@ defmodule Mix.Tasks.Pleroma.User do
 
       mix pleroma.user unsubscribe NICKNAME
 
+  ## Unsubscribe local users from an entire instance and deactivate all accounts
+
+      mix pleroma.user unsubscribe_all_from_instance INSTANCE
+
   ## Create a password reset link.
 
       mix pleroma.user reset_password NICKNAME
@@ -246,6 +250,20 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
+  def run(["unsubscribe_all_from_instance", instance]) do
+    start_pleroma()
+
+    Pleroma.User.Query.build(%{nickname: "@#{instance}"})
+    |> Pleroma.RepoStreamer.chunk_stream(500)
+    |> Stream.each(fn users ->
+      users
+      |> Enum.each(fn user ->
+        run(["unsubscribe", user.nickname])
+      end)
+    end)
+    |> Stream.run()
+  end
+
   def run(["set", nickname | rest]) do
     start_pleroma()
 
index ba4cf8486c9f0d773b3440641a81428b242e7be8..0353314914ba82c35d062adde2cf6db7caeedf46 100644 (file)
@@ -140,6 +140,11 @@ defmodule Pleroma.Application do
             id: :federator_init,
             start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
             restart: :temporary
+          },
+          %{
+            id: :internal_fetch_init,
+            start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
+            restart: :temporary
           }
         ] ++
         streamer_child() ++
index 3c13a055800d51479b53470140f36c0a9cac2aa6..7799b2a7887f5c1d3ce25254c85a070eb002e76e 100644 (file)
@@ -35,7 +35,7 @@ defmodule Pleroma.Config.TransferTask do
         if String.starts_with?(setting.key, "Pleroma.") do
           "Elixir." <> setting.key
         else
-          setting.key
+          String.trim_leading(setting.key, ":")
         end
 
       group = String.to_existing_atom(setting.group)
index c216cdcb11af5875d47e602b25fa327d07f88614..a1460d3038af6bae452259f50e2972e4925e51f4 100644 (file)
@@ -29,7 +29,7 @@ defmodule Pleroma.HTTP.Connection do
 
   # fetch Hackney options
   #
-  defp hackney_options(opts) do
+  def hackney_options(opts) do
     options = Keyword.get(opts, :adapter, [])
     adapter_options = Pleroma.Config.get([:http, :adapter], [])
     proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
index c96ee7353db19225f367d7afb900efcb8b9629f2..dec24458a82b0a8c04f094aa7553a4dd54cf9e2b 100644 (file)
@@ -65,10 +65,7 @@ defmodule Pleroma.HTTP do
   end
 
   def process_request_options(options) do
-    case Pleroma.Config.get([:http, :proxy_url]) do
-      nil -> options
-      proxy -> options ++ [proxy: proxy]
-    end
+    Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
   end
 
   @doc """
index b7bc7a4da15eddce5c6bbc23e841f6f388f09a76..6dd31d3bdc3fb3d88348e03f3c6c34e65f40f21e 100644 (file)
@@ -35,10 +35,12 @@ defmodule Pleroma.Keys do
   end
 
   def keys_from_pem(pem) do
-    [private_key_code] = :public_key.pem_decode(pem)
-    private_key = :public_key.pem_entry_decode(private_key_code)
-    {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
-    public_key = {:RSAPublicKey, modulus, exponent}
-    {:ok, private_key, public_key}
+    with [private_key_code] <- :public_key.pem_decode(pem),
+         private_key <- :public_key.pem_entry_decode(private_key_code),
+         {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
+      {:ok, private_key, {:RSAPublicKey, modulus, exponent}}
+    else
+      error -> {:error, error}
+    end
   end
 end
index a5b1cad680ddf20dc3eb23e5981917cfa765a840..1d320206e3091b68ec61535247666f42f2232700 100644 (file)
@@ -16,6 +16,7 @@ defmodule Pleroma.List do
     belongs_to(:user, User, type: Pleroma.FlakeId)
     field(:title, :string)
     field(:following, {:array, :string}, default: [])
+    field(:ap_id, :string)
 
     timestamps()
   end
@@ -55,6 +56,10 @@ defmodule Pleroma.List do
     Repo.one(query)
   end
 
+  def get_by_ap_id(ap_id) do
+    Repo.get_by(__MODULE__, ap_id: ap_id)
+  end
+
   def get_following(%Pleroma.List{following: following} = _list) do
     q =
       from(
@@ -105,7 +110,14 @@ defmodule Pleroma.List do
 
   def create(title, %User{} = creator) do
     list = %Pleroma.List{user_id: creator.id, title: title}
-    Repo.insert(list)
+
+    Repo.transaction(fn ->
+      list = Repo.insert!(list)
+
+      list
+      |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}")
+      |> Repo.update!()
+    end)
   end
 
   def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
@@ -125,4 +137,19 @@ defmodule Pleroma.List do
     |> follow_changeset(attrs)
     |> Repo.update()
   end
+
+  def memberships(%User{follower_address: follower_address}) do
+    Pleroma.List
+    |> where([l], ^follower_address in l.following)
+    |> select([l], l.ap_id)
+    |> Repo.all()
+  end
+
+  def memberships(_), do: []
+
+  def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do
+    Enum.member?(following, follower_address)
+  end
+
+  def member?(_, _), do: false
 end
index a414afbbfe9e4107b30ce10306cef0e00960e3cc..d472292581201ebcfd79abd1628add4d4b03566f 100644 (file)
@@ -11,7 +11,6 @@ defmodule Pleroma.Notification do
   alias Pleroma.Pagination
   alias Pleroma.Repo
   alias Pleroma.User
-  alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.Push
   alias Pleroma.Web.Streamer
@@ -32,31 +31,47 @@ defmodule Pleroma.Notification do
     |> cast(attrs, [:seen])
   end
 
-  def for_user_query(user) do
-    Notification
-    |> where(user_id: ^user.id)
-    |> where(
-      [n, a],
-      fragment(
-        "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
-        a.actor
-      )
-    )
-    |> join(:inner, [n], activity in assoc(n, :activity))
-    |> join(:left, [n, a], object in Object,
-      on:
+  def for_user_query(user, opts) do
+    query =
+      Notification
+      |> where(user_id: ^user.id)
+      |> where(
+        [n, a],
         fragment(
-          "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
-          object.data,
-          a.data
+          "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
+          a.actor
         )
-    )
-    |> preload([n, a, o], activity: {a, object: o})
+      )
+      |> join(:inner, [n], activity in assoc(n, :activity))
+      |> join(:left, [n, a], object in Object,
+        on:
+          fragment(
+            "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
+            object.data,
+            a.data
+          )
+      )
+      |> preload([n, a, o], activity: {a, object: o})
+
+    if opts[:with_muted] do
+      query
+    else
+      where(query, [n, a], a.actor not in ^user.info.muted_notifications)
+      |> where([n, a], a.actor not in ^user.info.blocks)
+      |> where(
+        [n, a],
+        fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
+      )
+      |> join(:left, [n, a], tm in Pleroma.ThreadMute,
+        on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
+      )
+      |> where([n, a, o, tm], is_nil(tm.user_id))
+    end
   end
 
   def for_user(user, opts \\ %{}) do
     user
-    |> for_user_query()
+    |> for_user_query(opts)
     |> Pagination.fetch_paginated(opts)
   end
 
@@ -179,11 +194,10 @@ defmodule Pleroma.Notification do
 
   def get_notified_from_activity(_, _local_only), do: []
 
+  @spec skip?(Activity.t(), User.t()) :: boolean()
   def skip?(activity, user) do
     [
       :self,
-      :blocked,
-      :muted,
       :followers,
       :follows,
       :non_followers,
@@ -193,21 +207,11 @@ defmodule Pleroma.Notification do
     |> Enum.any?(&skip?(&1, activity, user))
   end
 
+  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
   def skip?(:self, activity, user) do
     activity.data["actor"] == user.ap_id
   end
 
-  def skip?(:blocked, activity, user) do
-    actor = activity.data["actor"]
-    User.blocks?(user, %{ap_id: actor})
-  end
-
-  def skip?(:muted, activity, user) do
-    actor = activity.data["actor"]
-
-    User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity)
-  end
-
   def skip?(
         :followers,
         activity,
index ada9da0bb6de7a0bdcb144b789223b939bd5deb9..f077a9f32436841f1ffb01975c970d3082fd80a7 100644 (file)
@@ -48,6 +48,9 @@ defmodule Pleroma.Object.Containment do
     end
   end
 
+  def contain_origin(id, %{"attributedTo" => actor} = params),
+    do: contain_origin(id, Map.put(params, "actor", actor))
+
   def contain_origin_from_id(_id, %{"id" => nil}), do: :error
 
   def contain_origin_from_id(id, %{"id" => other_id} = _params) do
@@ -60,4 +63,9 @@ defmodule Pleroma.Object.Containment do
       :error
     end
   end
+
+  def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
+    do: contain_origin(id, object)
+
+  def contain_child(_), do: :ok
 end
index 1e60d00824f3381b4c14da7f652b122b49befcb8..8d79ddb1fd5af72d79d33fdf2175ef9eb5dc84dc 100644 (file)
@@ -6,6 +6,8 @@ defmodule Pleroma.Object.Fetcher do
   alias Pleroma.HTTP
   alias Pleroma.Object
   alias Pleroma.Object.Containment
+  alias Pleroma.Signature
+  alias Pleroma.Web.ActivityPub.InternalFetchActor
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.OStatus
 
@@ -32,33 +34,39 @@ defmodule Pleroma.Object.Fetcher do
     else
       Logger.info("Fetching #{id} via AP")
 
-      with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
-           nil <- Object.normalize(data, false),
+      with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
+           {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
            params <- %{
              "type" => "Create",
              "to" => data["to"],
              "cc" => data["cc"],
+             # Should we seriously keep this attributedTo thing?
              "actor" => data["actor"] || data["attributedTo"],
              "object" => data
            },
-           :ok <- Containment.contain_origin(id, params),
+           {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
            {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
            {:object, _data, %Object{} = object} <-
              {:object, data, Object.normalize(activity, false)} do
         {:ok, object}
       else
+        {:containment, _} ->
+          {:error, "Object containment failed."}
+
         {:error, {:reject, nil}} ->
           {:reject, nil}
 
         {:object, data, nil} ->
           reinject_object(data)
 
-        object = %Object{} ->
+        {:normalize, object = %Object{}} ->
           {:ok, object}
 
         _e ->
+          # Only fallback when receiving a fetch/normalization error with ActivityPub
           Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
 
+          # FIXME: OStatus Object Containment?
           case OStatus.fetch_activity_from_url(id) do
             {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
             e -> e
@@ -76,15 +84,52 @@ defmodule Pleroma.Object.Fetcher do
     end
   end
 
+  defp make_signature(id, date) do
+    uri = URI.parse(id)
+
+    signature =
+      InternalFetchActor.get_actor()
+      |> Signature.sign(%{
+        "(request-target)": "get #{uri.path}",
+        host: uri.host,
+        date: date
+      })
+
+    [{:Signature, signature}]
+  end
+
+  defp sign_fetch(headers, id, date) do
+    if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
+      headers ++ make_signature(id, date)
+    else
+      headers
+    end
+  end
+
+  defp maybe_date_fetch(headers, date) do
+    if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
+      headers ++ [{:Date, date}]
+    else
+      headers
+    end
+  end
+
   def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
     Logger.info("Fetching object #{id} via AP")
 
+    date =
+      NaiveDateTime.utc_now()
+      |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
+
+    headers =
+      [{:Accept, "application/activity+json"}]
+      |> maybe_date_fetch(date)
+      |> sign_fetch(id, date)
+
+    Logger.debug("Fetch headers: #{inspect(headers)}")
+
     with true <- String.starts_with?(id, "http"),
-         {:ok, %{body: body, status: code}} when code in 200..299 <-
-           HTTP.get(
-             id,
-             [{:Accept, "application/activity+json"}]
-           ),
+         {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
          {:ok, data} <- Jason.decode(body),
          :ok <- Containment.contain_origin_from_id(id, data) do
       {:ok, data}
@@ -97,8 +142,8 @@ defmodule Pleroma.Object.Fetcher do
     end
   end
 
-  def fetch_and_contain_remote_object_from_id(%{"id" => id), do: fetch_and_contain_remote_object_from_id(id)
+  def fetch_and_contain_remote_object_from_id(%{"id" => id}),
+    do: fetch_and_contain_remote_object_from_id(id)
+
   def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
-    {:error, "id must be a string"}
-  end
 end
index da4ed4226620215ea1ca50f3e98588fc5088aaeb..567674a0b1d5953804050429f9ccbe8a60395563 100644 (file)
@@ -6,9 +6,21 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
   alias Comeonin.Pbkdf2
   import Plug.Conn
   alias Pleroma.User
+  require Logger
 
-  def init(options) do
-    options
+  def init(options), do: options
+
+  def checkpw(password, "$6" <> _ = password_hash) do
+    :crypt.crypt(password, password_hash) == password_hash
+  end
+
+  def checkpw(password, "$pbkdf2" <> _ = password_hash) do
+    Pbkdf2.checkpw(password, password_hash)
+  end
+
+  def checkpw(_password, _password_hash) do
+    Logger.error("Password hash not recognized")
+    false
   end
 
   def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
index e2874c469191ff1f8af1582b45fb3a38ccd7ea42..d87fa52fa521b1df96df165491ddddf9073c7d3c 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
-  alias Pleroma.Web.ActivityPub.Utils
   import Plug.Conn
   require Logger
 
@@ -16,38 +15,30 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
   end
 
   def call(conn, _opts) do
-    user = Utils.get_ap_id(conn.params["actor"])
-    Logger.debug("Checking sig for #{user}")
     [signature | _] = get_req_header(conn, "signature")
 
-    cond do
-      signature && String.contains?(signature, user) ->
-        # set (request-target) header to the appropriate value
-        # we also replace the digest header with the one we computed
-        conn =
-          conn
-          |> put_req_header(
-            "(request-target)",
-            String.downcase("#{conn.method}") <> " #{conn.request_path}"
-          )
-
-        conn =
-          if conn.assigns[:digest] do
-            conn
-            |> put_req_header("digest", conn.assigns[:digest])
-          else
-            conn
-          end
-
-        assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+    if signature do
+      # set (request-target) header to the appropriate value
+      # we also replace the digest header with the one we computed
+      conn =
+        conn
+        |> put_req_header(
+          "(request-target)",
+          String.downcase("#{conn.method}") <> " #{conn.request_path}"
+        )
 
-      signature ->
-        Logger.debug("Signature not from actor")
-        assign(conn, :valid_signature, false)
+      conn =
+        if conn.assigns[:digest] do
+          conn
+          |> put_req_header("digest", conn.assigns[:digest])
+        else
+          conn
+        end
 
-      true ->
-        Logger.debug("No signature header!")
-        conn
+      assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+    else
+      Logger.debug("No signature header!")
+      conn
     end
   end
 end
diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex
new file mode 100644 (file)
index 0000000..ce8494b
--- /dev/null
@@ -0,0 +1,70 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
+  alias Pleroma.Signature
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.Utils
+
+  import Plug.Conn
+  require Logger
+
+  def init(options), do: options
+
+  defp key_id_from_conn(conn) do
+    with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do
+      Signature.key_id_to_actor_id(key_id)
+    else
+      _ ->
+        nil
+    end
+  end
+
+  defp user_from_key_id(conn) do
+    with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
+         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
+      user
+    else
+      _ ->
+        nil
+    end
+  end
+
+  def call(%{assigns: %{user: _}} = conn, _opts), do: conn
+
+  # if this has payload make sure it is signed by the same actor that made it
+  def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
+    with actor_id <- Utils.get_ap_id(actor),
+         {:user, %User{} = user} <- {:user, user_from_key_id(conn)},
+         {:user_match, true} <- {:user_match, user.ap_id == actor_id} do
+      assign(conn, :user, user)
+    else
+      {:user_match, false} ->
+        Logger.debug("Failed to map identity from signature (payload actor mismatch)")
+        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
+        assign(conn, :valid_signature, false)
+
+      # remove me once testsuite uses mapped capabilities instead of what we do now
+      {:user, nil} ->
+        Logger.debug("Failed to map identity from signature (lookup failure)")
+        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
+        conn
+    end
+  end
+
+  # no payload, probably a signed fetch
+  def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
+    with %User{} = user <- user_from_key_id(conn) do
+      assign(conn, :user, user)
+    else
+      _ ->
+        Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
+        Logger.debug("key_id=#{key_id_from_conn(conn)}")
+        assign(conn, :valid_signature, false)
+    end
+  end
+
+  # no signature at all
+  def call(conn, _opts), do: conn
+end
index c5e0957e8823b2369ed414c26f2a57348b26821d..31388f574c1bfa70c2b19171b50a763e672217fb 100644 (file)
@@ -31,12 +31,28 @@ defmodule Pleroma.Plugs.RateLimiter do
 
   ## Usage
 
+  AllowedSyntax:
+
+      plug(Pleroma.Plugs.RateLimiter, :limiter_name)
+      plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
+
+  Allowed options:
+
+      * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
+      * `params` appends values of specified request params (e.g. ["id"]) to bucket name
+
   Inside a controller:
 
       plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
       plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
 
-  or inside a router pipiline:
+      plug(
+        Pleroma.Plugs.RateLimiter,
+        {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+        when action in ~w(fav_status unfav_status)a
+      )
+
+  or inside a router pipeline:
 
       pipeline :api do
         ...
@@ -49,33 +65,56 @@ defmodule Pleroma.Plugs.RateLimiter do
 
   alias Pleroma.User
 
-  def init(limiter_name) do
+  def init(limiter_name) when is_atom(limiter_name) do
+    init({limiter_name, []})
+  end
+
+  def init({limiter_name, opts}) do
     case Pleroma.Config.get([:rate_limit, limiter_name]) do
       nil -> nil
-      config -> {limiter_name, config}
+      config -> {limiter_name, config, opts}
     end
   end
 
-  # do not limit if there is no limiter configuration
+  # Do not limit if there is no limiter configuration
   def call(conn, nil), do: conn
 
-  def call(conn, opts) do
-    case check_rate(conn, opts) do
-      {:ok, _count} -> conn
-      {:error, _count} -> render_throttled_error(conn)
+  def call(conn, settings) do
+    case check_rate(conn, settings) do
+      {:ok, _count} ->
+        conn
+
+      {:error, _count} ->
+        render_throttled_error(conn)
+    end
+  end
+
+  defp bucket_name(conn, limiter_name, opts) do
+    bucket_name = opts[:bucket_name] || limiter_name
+
+    if params_names = opts[:params] do
+      params_values = for p <- Enum.sort(params_names), do: conn.params[p]
+      Enum.join([bucket_name] ++ params_values, ":")
+    else
+      bucket_name
     end
   end
 
-  defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do
-    ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit)
+  defp check_rate(
+         %{assigns: %{user: %User{id: user_id}}} = conn,
+         {limiter_name, [_, {scale, limit}], opts}
+       ) do
+    bucket_name = bucket_name(conn, limiter_name, opts)
+    ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
   end
 
-  defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do
-    ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit)
+  defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
+    bucket_name = bucket_name(conn, limiter_name, opts)
+    ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
   end
 
-  defp check_rate(conn, {limiter_name, {scale, limit}}) do
-    check_rate(conn, {limiter_name, [{scale, limit}]})
+  defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
+    check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
   end
 
   def ip(%{remote_ip: remote_ip}) do
index bf31e9cba26a1b2919732b75f3237fe444b9a622..1f98f215ca343d0e7e83d5e7f3eea974b5c3a022 100644 (file)
@@ -61,7 +61,7 @@ defmodule Pleroma.ReverseProxy do
   * `http`: options for [hackney](https://github.com/benoitc/hackney).
 
   """
-  @default_hackney_options []
+  @default_hackney_options [pool: :media]
 
   @inline_content_types [
     "image/gif",
@@ -94,7 +94,8 @@ defmodule Pleroma.ReverseProxy do
 
   def call(conn = %{method: method}, url, opts) when method in @methods do
     hackney_opts =
-      @default_hackney_options
+      Pleroma.HTTP.Connection.hackney_options([])
+      |> Keyword.merge(@default_hackney_options)
       |> Keyword.merge(Keyword.get(opts, :http, []))
       |> HTTP.process_request_options()
 
index 1a4d54c62ce030534429a0b6b493121461118200..2a0823ecf73f4580f9f559ba580da5c639a5880a 100644 (file)
@@ -8,10 +8,16 @@ defmodule Pleroma.Signature do
   alias Pleroma.Keys
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
-  alias Pleroma.Web.ActivityPub.Utils
+
+  def key_id_to_actor_id(key_id) do
+    URI.parse(key_id)
+    |> Map.put(:fragment, nil)
+    |> URI.to_string()
+  end
 
   def fetch_public_key(conn) do
-    with actor_id <- Utils.get_ap_id(conn.params["actor"]),
+    with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
+         actor_id <- key_id_to_actor_id(kid),
          {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
       {:ok, public_key}
     else
@@ -21,7 +27,8 @@ defmodule Pleroma.Signature do
   end
 
   def refetch_public_key(conn) do
-    with actor_id <- Utils.get_ap_id(conn.params["actor"]),
+    with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
+         actor_id <- key_id_to_actor_id(kid),
          {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
          {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
       {:ok, public_key}
index e4c2258334e3a4aa754d7bbffd232df7f3be0feb..14928c355484165dd06f544fc4ce80a7bbf034c1 100644 (file)
@@ -6,10 +6,19 @@ defmodule Pleroma.Upload.Filter.Dedupe do
   @behaviour Pleroma.Upload.Filter
   alias Pleroma.Upload
 
-  def filter(%Upload{name: name} = upload) do
-    extension = String.split(name, ".") |> List.last()
-    shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
+  def filter(%Upload{name: name, tempfile: tempfile} = upload) do
+    extension =
+      name
+      |> String.split(".")
+      |> List.last()
+
+    shasum =
+      :crypto.hash(:sha256, File.read!(tempfile))
+      |> Base.encode16(case: :lower)
+
     filename = shasum <> "." <> extension
     {:ok, %Upload{upload | id: shasum, path: filename}}
   end
+
+  def filter(_), do: :ok
 end
index 35a5a13814807f1a787c1e136fd51aef3bf245d2..fee49fb5100590cc50f0e0c742e2506d05eafd64 100644 (file)
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Upload.Filter.Mogrifun do
   @behaviour Pleroma.Upload.Filter
+  alias Pleroma.Upload.Filter
 
   @filters [
     {"implode", "1"},
@@ -34,31 +35,10 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
   ]
 
   def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
-    filter = Enum.random(@filters)
-
-    file
-    |> Mogrify.open()
-    |> mogrify_filter(filter)
-    |> Mogrify.save(in_place: true)
+    Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
 
     :ok
   end
 
   def filter(_), do: :ok
-
-  defp mogrify_filter(mogrify, [filter | rest]) do
-    mogrify
-    |> mogrify_filter(filter)
-    |> mogrify_filter(rest)
-  end
-
-  defp mogrify_filter(mogrify, []), do: mogrify
-
-  defp mogrify_filter(mogrify, {action, options}) do
-    Mogrify.custom(mogrify, action, options)
-  end
-
-  defp mogrify_filter(mogrify, string) when is_binary(string) do
-    Mogrify.custom(mogrify, string)
-  end
 end
index f459eeecbf617645af0a48beca0f18ccdda8f897..91bfdd4f5bdf7157a8036ad9c8ecfe6c78d203f8 100644 (file)
@@ -11,16 +11,19 @@ defmodule Pleroma.Upload.Filter.Mogrify do
   def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
     filters = Pleroma.Config.get!([__MODULE__, :args])
 
+    do_filter(file, filters)
+    :ok
+  end
+
+  def filter(_), do: :ok
+
+  def do_filter(file, filters) do
     file
     |> Mogrify.open()
     |> mogrify_filter(filters)
     |> Mogrify.save(in_place: true)
-
-    :ok
   end
 
-  def filter(_), do: :ok
-
   defp mogrify_filter(mogrify, nil), do: mogrify
 
   defp mogrify_filter(mogrify, [filter | rest]) do
index 0af76bc593f4e69d245678686885cf7a5f32268e..c0b22c28a4cf7af16b37767f12b7fb2ff9808a6b 100644 (file)
@@ -68,7 +68,14 @@ defmodule Pleroma.Uploaders.Uploader do
             {:error, error}
         end
     after
-      30_000 -> {:error, dgettext("errors", "Uploader callback timeout")}
+      callback_timeout() -> {:error, dgettext("errors", "Uploader callback timeout")}
+    end
+  end
+
+  defp callback_timeout do
+    case Mix.env() do
+      :test -> 1_000
+      _ -> 30_000
     end
   end
 end
index 956ec6240ddd2b7873afd70bdcbd8332979c68bb..f5f7c6c0443840b83a4fb395928e98a1e76a11c0 100644 (file)
@@ -707,9 +707,7 @@ defmodule Pleroma.User do
       user
     else
       e ->
-        Logger.error(
-          "Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}"
-        )
+        Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
 
         user
     end
@@ -798,10 +796,13 @@ defmodule Pleroma.User do
     |> Repo.all()
   end
 
-  def mute(muter, %User{ap_id: ap_id}) do
+  @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
+  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
+    info = muter.info
+
     info_cng =
-      muter.info
-      |> User.Info.add_to_mutes(ap_id)
+      User.Info.add_to_mutes(info, ap_id)
+      |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
 
     cng =
       change(muter)
@@ -811,9 +812,11 @@ defmodule Pleroma.User do
   end
 
   def unmute(muter, %{ap_id: ap_id}) do
+    info = muter.info
+
     info_cng =
-      muter.info
-      |> User.Info.remove_from_mutes(ap_id)
+      User.Info.remove_from_mutes(info, ap_id)
+      |> User.Info.remove_from_muted_notifications(info, ap_id)
 
     cng =
       change(muter)
@@ -909,6 +912,12 @@ defmodule Pleroma.User do
   def mutes?(nil, _), do: false
   def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
 
+  @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
+  def muted_notifications?(nil, _), do: false
+
+  def muted_notifications?(user, %{ap_id: ap_id}),
+    do: Enum.member?(user.info.muted_notifications, ap_id)
+
   def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
     blocks = info.blocks
     domain_blocks = info.domain_blocks
@@ -1195,19 +1204,18 @@ defmodule Pleroma.User do
     end
   end
 
-  def get_or_create_instance_user do
-    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
-
-    if user = get_cached_by_ap_id(relay_uri) do
+  @doc "Creates an internal service actor by URI if missing.  Optionally takes nickname for addressing."
+  def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
+    if user = get_cached_by_ap_id(uri) do
       user
     else
       changes =
         %User{info: %User.Info{}}
         |> cast(%{}, [:ap_id, :nickname, :local])
-        |> put_change(:ap_id, relay_uri)
-        |> put_change(:nickname, nil)
+        |> put_change(:ap_id, uri)
+        |> put_change(:nickname, nickname)
         |> put_change(:local, true)
-        |> put_change(:follower_address, relay_uri <> "/followers")
+        |> put_change(:follower_address, uri <> "/followers")
 
       {:ok, user} = Repo.insert(changes)
       user
@@ -1228,10 +1236,12 @@ defmodule Pleroma.User do
   end
 
   # OStatus Magic Key
-  def public_key_from_info(%{magic_key: magic_key}) do
+  def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
     {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
   end
 
+  def public_key_from_info(_), do: {:error, "not found key"}
+
   def get_public_key_for_ap_id(ap_id) do
     with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
          {:ok, public_key} <- public_key_from_info(user.info) do
@@ -1248,7 +1258,7 @@ defmodule Pleroma.User do
     data
     |> Map.put(:name, blank?(data[:name]) || data[:nickname])
     |> remote_user_creation()
-    |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
+    |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
     |> set_cache()
   end
 
@@ -1417,23 +1427,16 @@ defmodule Pleroma.User do
     }
   end
 
-  def ensure_keys_present(user) do
-    info = user.info
-
+  def ensure_keys_present(%User{info: info} = user) do
     if info.keys do
       {:ok, user}
     else
       {:ok, pem} = Keys.generate_rsa_pem()
 
-      info_cng =
-        info
-        |> User.Info.set_keys(pem)
-
-      cng =
-        Ecto.Changeset.change(user)
-        |> Ecto.Changeset.put_embed(:info, info_cng)
-
-      update_and_set_cache(cng)
+      user
+      |> Ecto.Changeset.change()
+      |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
+      |> update_and_set_cache()
     end
   end
 
@@ -1454,4 +1457,8 @@ defmodule Pleroma.User do
   end
 
   defp put_password_hash(changeset), do: changeset
+
+  def is_internal_user?(%User{nickname: nil}), do: true
+  def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
+  def is_internal_user?(_), do: false
 end
index 4cc3f2f2c0b4ada7f7223ff46c5b457a09ad40ac..b03e705c35f00b1bf9843a35d60cacd0ef86eb60 100644 (file)
@@ -26,6 +26,7 @@ defmodule Pleroma.User.Info do
     field(:domain_blocks, {:array, :string}, default: [])
     field(:mutes, {:array, :string}, default: [])
     field(:muted_reblogs, {:array, :string}, default: [])
+    field(:muted_notifications, {:array, :string}, default: [])
     field(:subscribers, {:array, :string}, default: [])
     field(:deactivated, :boolean, default: false)
     field(:no_rich_text, :boolean, default: false)
@@ -122,6 +123,16 @@ defmodule Pleroma.User.Info do
     |> validate_required([:mutes])
   end
 
+  @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t()
+  def set_notification_mutes(changeset, muted_notifications, notifications?) do
+    if notifications? do
+      put_change(changeset, :muted_notifications, muted_notifications)
+      |> validate_required([:muted_notifications])
+    else
+      changeset
+    end
+  end
+
   def set_blocks(info, blocks) do
     params = %{blocks: blocks}
 
@@ -138,14 +149,31 @@ defmodule Pleroma.User.Info do
     |> validate_required([:subscribers])
   end
 
+  @spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
   def add_to_mutes(info, muted) do
     set_mutes(info, Enum.uniq([muted | info.mutes]))
   end
 
+  @spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
+          Changeset.t()
+  def add_to_muted_notifications(changeset, info, muted, notifications?) do
+    set_notification_mutes(
+      changeset,
+      Enum.uniq([muted | info.muted_notifications]),
+      notifications?
+    )
+  end
+
+  @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
   def remove_from_mutes(info, muted) do
     set_mutes(info, List.delete(info.mutes, muted))
   end
 
+  @spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
+  def remove_from_muted_notifications(changeset, info, muted) do
+    set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
+  end
+
   def add_to_block(info, blocked) do
     set_blocks(info, Enum.uniq([blocked | info.blocks]))
   end
index 2dd9dbf7fa2c28afa3c788332f87b67b93cb5ddb..ba7fc1e012dfd27905f71883a953e3a47e0e7a6b 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.Conversation
   alias Pleroma.Notification
   alias Pleroma.Object
+  alias Pleroma.Object.Containment
   alias Pleroma.Object.Fetcher
   alias Pleroma.Pagination
   alias Pleroma.Repo
@@ -26,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   # For Announce activities, we filter the recipients based on following status for any actors
   # that match actual users.  See issue #164 for more information about why this is necessary.
   defp get_recipients(%{"type" => "Announce"} = data) do
-    to = data["to"] || []
-    cc = data["cc"] || []
+    to = Map.get(data, "to", [])
+    cc = Map.get(data, "cc", [])
+    bcc = Map.get(data, "bcc", [])
     actor = User.get_cached_by_ap_id(data["actor"])
 
     recipients =
-      (to ++ cc)
-      |> Enum.filter(fn recipient ->
+      Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
         case User.get_cached_by_ap_id(recipient) do
-          nil ->
-            true
-
-          user ->
-            User.following?(user, actor)
+          nil -> true
+          user -> User.following?(user, actor)
         end
       end)
 
@@ -46,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   defp get_recipients(%{"type" => "Create"} = data) do
-    to = data["to"] || []
-    cc = data["cc"] || []
-    actor = data["actor"] || []
-    recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
+    to = Map.get(data, "to", [])
+    cc = Map.get(data, "cc", [])
+    bcc = Map.get(data, "bcc", [])
+    actor = Map.get(data, "actor", [])
+    recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
     {recipients, to, cc}
   end
 
   defp get_recipients(data) do
-    to = data["to"] || []
-    cc = data["cc"] || []
-    recipients = to ++ cc
+    to = Map.get(data, "to", [])
+    cc = Map.get(data, "cc", [])
+    bcc = Map.get(data, "bcc", [])
+    recipients = Enum.concat([to, cc, bcc])
     {recipients, to, cc}
   end
 
@@ -126,6 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
          {:ok, map} <- MRF.filter(map),
          {recipients, _, _} = get_recipients(map),
          {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
+         :ok <- Containment.contain_child(map),
          {:ok, map, object} <- insert_full_object(map) do
       {:ok, activity} =
         Repo.insert(%Activity{
@@ -896,13 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp maybe_order(query, _), do: query
 
   def fetch_activities_query(recipients, opts \\ %{}) do
-    base_query = from(activity in Activity)
-
     config = %{
       skip_thread_containment: Config.get([:instance, :skip_thread_containment])
     }
 
-    base_query
+    Activity
     |> maybe_preload_objects(opts)
     |> maybe_preload_bookmarks(opts)
     |> maybe_set_thread_muted_field(opts)
@@ -931,11 +930,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   def fetch_activities(recipients, opts \\ %{}) do
-    fetch_activities_query(recipients, opts)
+    list_memberships = Pleroma.List.memberships(opts["user"])
+
+    fetch_activities_query(recipients ++ list_memberships, opts)
     |> Pagination.fetch_paginated(opts)
     |> Enum.reverse()
+    |> maybe_update_cc(list_memberships, opts["user"])
   end
 
+  defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
+       when is_list(list_memberships) and length(list_memberships) > 0 do
+    Enum.map(activities, fn
+      %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
+        if Enum.any?(bcc, &(&1 in list_memberships)) do
+          update_in(activity.data["cc"], &[user_ap_id | &1])
+        else
+          activity
+        end
+
+      activity ->
+        activity
+    end)
+  end
+
+  defp maybe_update_cc(activities, _, _), do: activities
+
   def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
     from(activity in query,
       where:
index e2af4ad1a7167286ef0cc9574ae7c9936d5689ea..133a726c5c05a352cbe1eb9375b95deabec48995 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   alias Pleroma.Object.Fetcher
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.InternalFetchActor
   alias Pleroma.Web.ActivityPub.ObjectView
   alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.ActivityPub.Transmogrifier
@@ -206,9 +207,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     json(conn, dgettext("errors", "error"))
   end
 
-  def relay(conn, _params) do
-    with %User{} = user <- Relay.get_actor(),
-         {:ok, user} <- User.ensure_keys_present(user) do
+  defp represent_service_actor(%User{} = user, conn) do
+    with {:ok, user} <- User.ensure_keys_present(user) do
       conn
       |> put_resp_header("content-type", "application/activity+json")
       |> json(UserView.render("user.json", %{user: user}))
@@ -217,6 +217,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     end
   end
 
+  defp represent_service_actor(nil, _), do: {:error, :not_found}
+
+  def relay(conn, _params) do
+    Relay.get_actor()
+    |> represent_service_actor(conn)
+  end
+
+  def internal_fetch(conn, _params) do
+    InternalFetchActor.get_actor()
+    |> represent_service_actor(conn)
+  end
+
   def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
     conn
     |> put_resp_header("content-type", "application/activity+json")
diff --git a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
new file mode 100644 (file)
index 0000000..9213ddd
--- /dev/null
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.InternalFetchActor do
+  alias Pleroma.User
+
+  require Logger
+
+  def init do
+    # Wait for everything to settle.
+    Process.sleep(1000 * 5)
+    get_actor()
+  end
+
+  def get_actor do
+    "#{Pleroma.Web.Endpoint.url()}/internal/fetch"
+    |> User.get_or_create_service_actor_by_ap_id("internal.fetch")
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
new file mode 100644 (file)
index 0000000..1842e1a
--- /dev/null
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
+  @moduledoc "Block messages which mention a user"
+
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  @impl true
+  def filter(%{"type" => "Create"} = message) do
+    reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
+    recipients = (message["to"] || []) ++ (message["cc"] || [])
+
+    if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
+      {:reject, nil}
+    else
+      {:ok, message}
+    end
+  end
+
+  @impl true
+  def filter(message), do: {:ok, message}
+end
index a05e032639ed542893f88eb1fafa6b7b48885028..c505223f751259d8adc8a5bb63d7ac007c3d5d48 100644 (file)
@@ -92,18 +92,68 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
     end
   end
 
-  @doc """
-  Publishes an activity to all relevant peers.
-  """
-  def publish(%User{} = actor, %Activity{} = activity) do
-    remote_followers =
+  defp recipients(actor, activity) do
+    followers =
       if actor.follower_address in activity.recipients do
         {:ok, followers} = User.get_followers(actor)
-        followers |> Enum.filter(&(!&1.local))
+        Enum.filter(followers, &(!&1.local))
       else
         []
       end
 
+    Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
+  end
+
+  defp get_cc_ap_ids(ap_id, recipients) do
+    host = Map.get(URI.parse(ap_id), :host)
+
+    recipients
+    |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
+    |> Enum.map(& &1.ap_id)
+  end
+
+  @doc """
+  Publishes an activity with BCC to all relevant peers.
+  """
+
+  def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do
+    public = is_public?(activity)
+    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+
+    recipients = recipients(actor, activity)
+
+    recipients
+    |> Enum.filter(&User.ap_enabled?/1)
+    |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
+    |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+    |> Instances.filter_reachable()
+    |> Enum.each(fn {inbox, unreachable_since} ->
+      %User{ap_id: ap_id} =
+        Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
+
+      # Get all the recipients on the same host and add them to cc. Otherwise, a remote
+      # instance would only accept a first message for the first recipient and ignore the rest.
+      cc = get_cc_ap_ids(ap_id, recipients)
+
+      json =
+        data
+        |> Map.put("cc", cc)
+        |> Jason.encode!()
+
+      Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
+        inbox: inbox,
+        json: json,
+        actor: actor,
+        id: activity.data["id"],
+        unreachable_since: unreachable_since
+      })
+    end)
+  end
+
+  @doc """
+  Publishes an activity to all relevant peers.
+  """
+  def publish(%User{} = actor, %Activity{} = activity) do
     public = is_public?(activity)
 
     if public && Config.get([:instance, :allow_relay]) do
@@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
     {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
     json = Jason.encode!(data)
 
-    (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
+    recipients(actor, activity)
     |> Enum.filter(fn user -> User.ap_enabled?(user) end)
     |> Enum.map(fn %{info: %{source_data: data}} ->
       (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
index 93808517bde9ad2af6c717de8022b0db9c513a7c..1ebfcdd86a67743c3e69b945772f762d2c79e6ed 100644 (file)
@@ -10,7 +10,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do
   require Logger
 
   def get_actor do
-    User.get_or_create_instance_user()
+    "#{Pleroma.Web.Endpoint.url()}/relay"
+    |> User.get_or_create_service_actor_by_ap_id()
   end
 
   def follow(target_instance) do
index 10b362908ff6a8e09163c65fa758383af06081f6..816ea8e353a21fe4e23e6941b5ec6b980f7fa5fe 100644 (file)
@@ -814,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
   def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
     object =
-      Object.normalize(object_id).data
+      object_id
+      |> Object.normalize()
+      |> Map.get(:data)
       |> prepare_object
 
     data =
       data
       |> Map.put("object", object)
       |> Map.merge(Utils.make_json_ld_header())
+      |> Map.delete("bcc")
 
     {:ok, data}
   end
index 4288ea4c838a8cf291394663291246bea3d7c9bd..c146f59d4b04bf96ae6ce8864ea10ec296514b44 100644 (file)
@@ -25,12 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
 
   # Some implementations send the actor URI as the actor field, others send the entire actor object,
   # so figure out what the actor's URI is based on what we have.
-  def get_ap_id(object) do
-    case object do
-      %{"id" => id} -> id
-      id -> id
-    end
-  end
+  def get_ap_id(%{"id" => id} = _), do: id
+  def get_ap_id(id), do: id
 
   def normalize_params(params) do
     Map.put(params, "actor", get_ap_id(params["actor"]))
index d9c1bcb2c4998ae7b8dfe0c1ed3d197c7f82b19a..639519e0a87692e6f8d3e9116b1c4a04b1791b5c 100644 (file)
@@ -31,8 +31,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
 
   def render("endpoints.json", _), do: %{}
 
-  # the instance itself is not a Person, but instead an Application
-  def render("user.json", %{user: %{nickname: nil} = user}) do
+  def render("service.json", %{user: user}) do
     {:ok, user} = User.ensure_keys_present(user)
     {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys)
     public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
@@ -47,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
       "followers" => "#{user.ap_id}/followers",
       "inbox" => "#{user.ap_id}/inbox",
       "name" => "Pleroma",
-      "summary" => "Virtual actor for Pleroma relay",
+      "summary" =>
+        "An internal service actor for this Pleroma instance.  No user-serviceable parts inside.",
       "url" => user.ap_id,
       "manuallyApprovesFollowers" => false,
       "publicKey" => %{
@@ -60,6 +60,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     |> Map.merge(Utils.make_json_ld_header())
   end
 
+  # the instance itself is not a Person, but instead an Application
+  def render("user.json", %{user: %User{nickname: nil} = user}),
+    do: render("service.json", %{user: user})
+
+  def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
+    do: render("service.json", %{user: user})
+
   def render("user.json", %{user: user}) do
     {:ok, user} = User.ensure_keys_present(user)
     {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys)
index 9908a2e75816151e7d203b7736a657a952230a5a..2666edc7ce0934302011cc748aafb0bf3959d413 100644 (file)
@@ -34,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
     !is_public?(activity) && !is_private?(activity)
   end
 
+  def is_list?(%{data: %{"listMessage" => _}}), do: true
+  def is_list?(_), do: false
+
+  def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
+
+  def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
+    user.ap_id in activity.data["to"] ||
+      list_ap_id
+      |> Pleroma.List.get_by_ap_id()
+      |> Pleroma.List.member?(user)
+  end
+
+  def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
+
   def visible_for_user?(activity, nil) do
     is_public?(activity)
   end
@@ -73,6 +87,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
       object.data["directMessage"] == true ->
         "direct"
 
+      is_binary(object.data["listMessage"]) ->
+        "list"
+
       length(cc) > 0 ->
         "private"
 
index a9164ad9837960c2ca9e9c7d8549517987967025..f4234b743c509909cc8c28cbe63dfe27aed7ce1f 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.Auth.PleromaAuthenticator do
-  alias Comeonin.Pbkdf2
+  alias Pleroma.Plugs.AuthenticationPlug
   alias Pleroma.Registration
   alias Pleroma.Repo
   alias Pleroma.User
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
   def get_user(%Plug.Conn{} = conn) do
     with {:ok, {name, password}} <- fetch_credentials(conn),
          {_, %User{} = user} <- {:user, fetch_user(name)},
-         {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
+         {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
       {:ok, user}
     else
       error ->
index f1450b1139178d940a78f854232613b67980ca9c..44af6a77341de80897b78b29e0f73ae04c7dc6fc 100644 (file)
@@ -4,7 +4,6 @@
 
 defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.Activity
-  alias Pleroma.Bookmark
   alias Pleroma.Formatter
   alias Pleroma.Object
   alias Pleroma.ThreadMute
@@ -31,7 +30,8 @@ defmodule Pleroma.Web.CommonAPI do
 
   def unfollow(follower, unfollowed) do
     with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
-         {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
+         {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
+         {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
       {:ok, follower}
     end
   end
@@ -175,6 +175,11 @@ defmodule Pleroma.Web.CommonAPI do
       when visibility in ~w{public unlisted private direct},
       do: {visibility, get_replied_to_visibility(in_reply_to)}
 
+  def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
+    visibility = {:list, String.to_integer(list_id)}
+    {visibility, get_replied_to_visibility(in_reply_to)}
+  end
+
   def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
     visibility = get_replied_to_visibility(in_reply_to)
     {visibility, visibility}
@@ -235,19 +240,18 @@ defmodule Pleroma.Web.CommonAPI do
              "emoji",
              Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
            ) do
-      res =
-        ActivityPub.create(
-          %{
-            to: to,
-            actor: user,
-            context: context,
-            object: object,
-            additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
-          },
-          Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
-        )
-
-      res
+      preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
+      direct? = visibility == "direct"
+
+      %{
+        to: to,
+        actor: user,
+        context: context,
+        object: object,
+        additional: %{"cc" => cc, "directMessage" => direct?}
+      }
+      |> maybe_add_list_data(user, visibility)
+      |> ActivityPub.create(preview?)
     else
       {:private_to_public, true} ->
         {:error, dgettext("errors", "The message visibility must be direct")}
@@ -351,15 +355,6 @@ defmodule Pleroma.Web.CommonAPI do
     end
   end
 
-  def bookmarked?(user, activity) do
-    with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
-      true
-    else
-      _ ->
-        false
-    end
-  end
-
   def report(user, data) do
     with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
          {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
index 8e482eef7b88f9e9520e40e70b039598e0ee3d01..fcc0009695bc4560af4c561081d9fa553f9544fe 100644 (file)
@@ -6,11 +6,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
   import Pleroma.Web.Gettext
 
   alias Calendar.Strftime
-  alias Comeonin.Pbkdf2
   alias Pleroma.Activity
   alias Pleroma.Config
   alias Pleroma.Formatter
   alias Pleroma.Object
+  alias Pleroma.Plugs.AuthenticationPlug
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.Utils
@@ -100,12 +100,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
+  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
+
   def get_addressed_users(_, to) when is_list(to) do
     User.get_ap_ids_by_nicknames(to)
   end
 
   def get_addressed_users(mentioned_users, _), do: mentioned_users
 
+  def maybe_add_list_data(activity_params, user, {:list, list_id}) do
+    case Pleroma.List.get(list_id, user) do
+      %Pleroma.List{} = list ->
+        activity_params
+        |> put_in([:additional, "bcc"], [list.ap_id])
+        |> put_in([:additional, "listMessage"], list.ap_id)
+        |> put_in([:object, "listMessage"], list.ap_id)
+
+      _ ->
+        activity_params
+    end
+  end
+
+  def maybe_add_list_data(activity_params, _, _), do: activity_params
+
   def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
       when is_list(options) do
     %{max_expiration: max_expiration, min_expiration: min_expiration} =
@@ -371,7 +388,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
 
   def confirm_current_password(user, password) do
     with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
-         true <- Pbkdf2.checkpw(password, db_user.password_hash) do
+         true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
       {:ok, db_user}
     else
       _ -> {:error, dgettext("errors", "Invalid password.")}
index c82b201233e31b5c1079fd7df8d28e0a1c667694..46944dcbc1381390eda5fbb48d45f401236f4b1b 100644 (file)
@@ -53,7 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
     options = cast_params(params)
 
     user
-    |> Notification.for_user_query()
+    |> Notification.for_user_query(options)
     |> restrict(:exclude_types, options)
     |> Pagination.fetch_paginated(params)
   end
@@ -67,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
   defp cast_params(params) do
     param_types = %{
       exclude_types: {:array, :string},
-      reblogs: :boolean
+      reblogs: :boolean,
+      with_muted: :boolean
     }
 
     changeset = cast({%{}, param_types}, params, Map.keys(param_types))
index 8c2033c3ab433ad8650638842e7c3c1b0ce8f076..e8b43e475dbde05edc9b5088713416f2f4129088 100644 (file)
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
   alias Pleroma.Stats
@@ -46,8 +47,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   require Logger
 
-  plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
-  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
+  @rate_limited_relations_actions ~w(follow unfollow)a
+
+  @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
+    post_status delete_status)a
+
+  plug(
+    RateLimiter,
+    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+    when action in ~w(reblog_status unreblog_status)a
+  )
+
+  plug(
+    RateLimiter,
+    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+    when action in ~w(fav_status unfav_status)a
+  )
+
+  plug(
+    RateLimiter,
+    {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
+  )
+
+  plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
+  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+  plug(RateLimiter, :app_account_creation when action == :account_register)
+  plug(RateLimiter, :search when action in [:search, :search2, :account_search])
+  plug(RateLimiter, :password_reset when action == :password_reset)
 
   @local_mastodon_name "Mastodon-Local"
 
@@ -414,7 +440,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-    with %User{} = user <- User.get_cached_by_id(params["id"]) do
+    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
       params =
         params
         |> Map.put("tag", params["tagged"])
@@ -676,11 +702,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       conn
       |> put_view(StatusView)
       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-    else
-      {:error, reason} ->
-        conn
-        |> put_status(:bad_request)
-        |> json(%{"error" => reason})
     end
   end
 
@@ -721,11 +742,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       conn
       |> put_view(StatusView)
       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-    else
-      {:error, reason} ->
-        conn
-        |> put_resp_content_type("application/json")
-        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
     end
   end
 
@@ -864,7 +880,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
          %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
       q = from(u in User, where: u.ap_id in ^likes)
       users = Repo.all(q)
@@ -878,7 +894,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
          %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
       q = from(u in User, where: u.ap_id in ^announces)
       users = Repo.all(q)
@@ -1051,9 +1067,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
+    notifications =
+      if Map.has_key?(params, "notifications"),
+        do: params["notifications"] in [true, "True", "true", "1"],
+        else: true
+
     with %User{} = muted <- User.get_cached_by_id(id),
-         {:ok, muter} <- User.mute(muter, muted) do
+         {:ok, muter} <- User.mute(muter, muted, notifications) do
       conn
       |> put_view(AccountView)
       |> render("relationship.json", %{user: muter, target: muted})
@@ -1629,6 +1650,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     render_error(conn, :not_found, "Record not found")
   end
 
+  def errors(conn, {:error, error_message}) do
+    conn
+    |> put_status(:bad_request)
+    |> json(%{error: error_message})
+  end
+
   def errors(conn, _) do
     conn
     |> put_status(:internal_server_error)
@@ -1790,6 +1817,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
+  def password_reset(conn, params) do
+    nickname_or_email = params["email"] || params["nickname"]
+
+    with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
+      conn
+      |> put_status(:no_content)
+      |> json("")
+    else
+      {:error, "unknown user"} ->
+        send_resp(conn, :not_found, "")
+
+      {:error, _} ->
+        send_resp(conn, :bad_request, "")
+    end
+  end
+
   def try_render(conn, target, params)
       when is_binary(target) do
     case render(conn, target, params) do
index 62c516f8eb97989538a1b3e3c3b0e050574ce50d..befb35c26b0cbb58218f30edf2ad0c86c82059b3 100644 (file)
@@ -51,8 +51,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
       following: User.following?(user, target),
       followed_by: User.following?(target, user),
       blocking: User.blocks?(user, target),
+      blocked_by: User.blocks?(target, user),
       muting: User.mutes?(user, target),
-      muting_notifications: false,
+      muting_notifications: User.muted_notifications?(user, target),
       subscribing: User.subscribed_to?(user, target),
       requested: requested,
       domain_blocking: false,
@@ -136,6 +137,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
     |> maybe_put_notification_settings(user, opts[:for])
     |> maybe_put_settings_store(user, opts[:for], opts)
     |> maybe_put_chat_token(user, opts[:for], opts)
+    |> maybe_put_activation_status(user, opts[:for])
   end
 
   defp username_from_nickname(string) when is_binary(string) do
@@ -196,6 +198,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
   defp maybe_put_notification_settings(data, _, _), do: data
 
+  defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
+    Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated)
+  end
+
+  defp maybe_put_activation_status(data, _, _), do: data
+
   defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
   defp image_url(_), do: nil
 end
index 06a7251d8f959e1ce1deca2c3ae2c0e8e809bbec..de942595941e29194ef38fdd38a62afc39ceedb6 100644 (file)
@@ -382,7 +382,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       %{
         # Mastodon uses separate ids for polls, but an object can't have
         # more than one poll embedded so object id is fine
-        id: object.id,
+        id: to_string(object.id),
         expires_at: Utils.to_masto_date(end_time),
         expired: expired,
         multiple: multiple,
index dd8888a021635357b4fe88c1bb2748517c7c6062..a661e9bb7153cf6f2c02b672bae88b06f5b481de 100644 (file)
@@ -3,68 +3,71 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.MediaProxy do
-  @base64_opts [padding: false]
-
-  def url(nil), do: nil
+  alias Pleroma.Config
+  alias Pleroma.Web
 
-  def url(""), do: nil
+  @base64_opts [padding: false]
 
+  def url(url) when is_nil(url) or url == "", do: nil
   def url("/" <> _ = url), do: url
 
   def url(url) do
-    if !enabled?() or local?(url) or whitelisted?(url) do
+    if disabled?() or local?(url) or whitelisted?(url) do
       url
     else
       encode_url(url)
     end
   end
 
-  defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false)
+  defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
 
   defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
 
   defp whitelisted?(url) do
     %{host: domain} = URI.parse(url)
 
-    Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
+    Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern ->
       String.equivalent?(domain, pattern)
     end)
   end
 
   def encode_url(url) do
-    secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
     base64 = Base.url_encode64(url, @base64_opts)
-    sig = :crypto.hmac(:sha, secret, base64)
-    sig64 = sig |> Base.url_encode64(@base64_opts)
+
+    sig64 =
+      base64
+      |> signed_url
+      |> Base.url_encode64(@base64_opts)
 
     build_url(sig64, base64, filename(url))
   end
 
   def decode_url(sig, url) do
-    secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
-    sig = Base.url_decode64!(sig, @base64_opts)
-    local_sig = :crypto.hmac(:sha, secret, url)
-
-    if local_sig == sig do
+    with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
+         signature when signature == sig <- signed_url(url) do
       {:ok, Base.url_decode64!(url, @base64_opts)}
     else
-      {:error, :invalid_signature}
+      _ -> {:error, :invalid_signature}
     end
   end
 
+  defp signed_url(url) do
+    :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url)
+  end
+
   def filename(url_or_path) do
     if path = URI.parse(url_or_path).path, do: Path.basename(path)
   end
 
   def build_url(sig_base64, url_base64, filename \\ nil) do
     [
-      Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()),
+      Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
       "proxy",
       sig_base64,
       url_base64,
       filename
     ]
-    |> Enum.filter(fn value -> value end)
+    |> Enum.filter(& &1)
     |> Path.join()
   end
 end
similarity index 71%
rename from lib/pleroma/web/media_proxy/controller.ex
rename to lib/pleroma/web/media_proxy/media_proxy_controller.ex
index ea33d7685752fd05edcc64c5e91e556a088e97c0..8403850ff1ca9b7c2f9ee9c56ffe93bab3f666a5 100644 (file)
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
     with config <- Pleroma.Config.get([:media_proxy], []),
          true <- Keyword.get(config, :enabled, false),
          {:ok, url} <- MediaProxy.decode_url(sig64, url64),
-         :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
+         :ok <- filename_matches(params, conn.request_path, url) do
       ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
     else
       false ->
@@ -27,13 +27,20 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
     end
   end
 
-  def filename_matches(has_filename, path, url) do
-    filename = url |> MediaProxy.filename()
+  def filename_matches(%{"filename" => _} = _, path, url) do
+    filename = MediaProxy.filename(url)
 
-    if has_filename && filename && Path.basename(path) != filename do
+    if filename && does_not_match(path, filename) do
       {:wrong_filename, filename}
     else
       :ok
     end
   end
+
+  def filename_matches(_, _, _), do: :ok
+
+  defp does_not_match(path, filename) do
+    basename = Path.basename(path)
+    basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
+  end
 end
index cd9a4f4a8c479816b76a6d875ac0b1d96935f034..a1d7fcc7dae34a046acebca76db2a72cc29ba7e1 100644 (file)
@@ -34,8 +34,11 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
   def raw_nodeinfo do
     stats = Stats.get_stats()
 
+    exclusions = Config.get([:instance, :mrf_transparency_exclusions])
+
     mrf_simple =
       Config.get(:mrf_simple)
+      |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
       |> Enum.into(%{})
 
     # This horror is needed to convert regex sigils to strings
@@ -86,7 +89,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
           mrf_simple: mrf_simple,
           mrf_keyword: mrf_keyword,
           mrf_user_allowlist: mrf_user_allowlist,
-          quarantined_instances: quarantined
+          quarantined_instances: quarantined,
+          exclusions: length(exclusions) > 0
         }
       else
         %{}
index 21cd47890abc8a17fa583241cdd8f970a91d3be4..b69b2be610a6383ed5f5c97a92f3db4c0fe3ea02 100644 (file)
@@ -3,12 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.RichMedia.Parser do
-  @parsers [
-    Pleroma.Web.RichMedia.Parsers.OGP,
-    Pleroma.Web.RichMedia.Parsers.TwitterCard,
-    Pleroma.Web.RichMedia.Parsers.OEmbed
-  ]
-
   @hackney_options [
     pool: :media,
     recv_timeout: 2_000,
@@ -16,6 +10,10 @@ defmodule Pleroma.Web.RichMedia.Parser do
     with_body: true
   ]
 
+  defp parsers do
+    Pleroma.Config.get([:rich_media, :parsers])
+  end
+
   def parse(nil), do: {:error, "No URL provided"}
 
   if Pleroma.Config.get(:env) == :test do
@@ -26,6 +24,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
         Cachex.fetch!(:rich_media_cache, url, fn _ ->
           {:commit, parse_url(url)}
         end)
+        |> set_ttl_based_on_image(url)
       rescue
         e ->
           {:error, "Cachex error: #{inspect(e)}"}
@@ -33,6 +32,50 @@ defmodule Pleroma.Web.RichMedia.Parser do
     end
   end
 
+  @doc """
+  Set the rich media cache based on the expiration time of image.
+
+  Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
+
+  ## Example
+
+      defmodule MyModule do
+        @behaviour Pleroma.Web.RichMedia.Parser.TTL
+        def ttl(data, url) do
+          image_url = Map.get(data, :image)
+          # do some parsing in the url and get the ttl of the image
+          # and return ttl is unix time
+          parse_ttl_from_url(image_url)
+        end
+      end
+
+  Define the module in the config
+
+      config :pleroma, :rich_media,
+        ttl_setters: [MyModule]
+  """
+  def set_ttl_based_on_image({:ok, data}, url) do
+    with {:ok, nil} <- Cachex.ttl(:rich_media_cache, url) do
+      ttl = get_ttl_from_image(data, url)
+      Cachex.expire_at(:rich_media_cache, url, ttl * 1000)
+      {:ok, data}
+    else
+      _ ->
+        {:ok, data}
+    end
+  end
+
+  defp get_ttl_from_image(data, url) do
+    Pleroma.Config.get([:rich_media, :ttl_setters])
+    |> Enum.reduce({:ok, nil}, fn
+      module, {:ok, _ttl} ->
+        module.ttl(data, url)
+
+      _, error ->
+        error
+    end)
+  end
+
   defp parse_url(url) do
     try do
       {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
@@ -48,7 +91,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
   end
 
   defp maybe_parse(html) do
-    Enum.reduce_while(@parsers, %{}, fn parser, acc ->
+    Enum.reduce_while(parsers(), %{}, fn parser, acc ->
       case parser.parse(html, acc) do
         {:ok, data} -> {:halt, data}
         {:error, _msg} -> {:cont, acc}
diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex
new file mode 100644 (file)
index 0000000..014c093
--- /dev/null
@@ -0,0 +1,52 @@
+defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
+  @behaviour Pleroma.Web.RichMedia.Parser.TTL
+
+  @impl Pleroma.Web.RichMedia.Parser.TTL
+  def ttl(data, _url) do
+    image = Map.get(data, :image)
+
+    if is_aws_signed_url(image) do
+      image
+      |> parse_query_params()
+      |> format_query_params()
+      |> get_expiration_timestamp()
+    end
+  end
+
+  defp is_aws_signed_url(""), do: nil
+  defp is_aws_signed_url(nil), do: nil
+
+  defp is_aws_signed_url(image) when is_binary(image) do
+    %URI{host: host, query: query} = URI.parse(image)
+
+    if String.contains?(host, "amazonaws.com") and
+         String.contains?(query, "X-Amz-Expires") do
+      image
+    else
+      nil
+    end
+  end
+
+  defp is_aws_signed_url(_), do: nil
+
+  defp parse_query_params(image) do
+    %URI{query: query} = URI.parse(image)
+    query
+  end
+
+  defp format_query_params(query) do
+    query
+    |> String.split(~r/&|=/)
+    |> Enum.chunk_every(2)
+    |> Map.new(fn [k, v] -> {k, v} end)
+  end
+
+  defp get_expiration_timestamp(params) when is_map(params) do
+    {:ok, date} =
+      params
+      |> Map.get("X-Amz-Date")
+      |> Timex.parse("{ISO:Basic:Z}")
+
+    Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))
+  end
+end
diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex
new file mode 100644 (file)
index 0000000..6b3ec6d
--- /dev/null
@@ -0,0 +1,3 @@
+defmodule Pleroma.Web.RichMedia.Parser.TTL do
+  @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()}
+end
index 3e5142e8a48b1aa054b983e1c6fdae63613334fd..518720d38c05f5af68abcc5f95c093150ff1d0eb 100644 (file)
@@ -586,7 +586,7 @@ defmodule Pleroma.Web.Router do
     end
   end
 
-  pipeline :ap_relay do
+  pipeline :ap_service_actor do
     plug(:accepts, ["activity+json", "json"])
   end
 
@@ -617,6 +617,7 @@ defmodule Pleroma.Web.Router do
   pipeline :activitypub do
     plug(:accepts, ["activity+json", "json"])
     plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
+    plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
   end
 
   scope "/", Pleroma.Web.ActivityPub do
@@ -663,8 +664,17 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/relay", Pleroma.Web.ActivityPub do
-    pipe_through(:ap_relay)
+    pipe_through(:ap_service_actor)
+
     get("/", ActivityPubController, :relay)
+    post("/inbox", ActivityPubController, :inbox)
+  end
+
+  scope "/internal/fetch", Pleroma.Web.ActivityPub do
+    pipe_through(:ap_service_actor)
+
+    get("/", ActivityPubController, :internal_fetch)
+    post("/inbox", ActivityPubController, :inbox)
   end
 
   scope "/", Pleroma.Web.ActivityPub do
@@ -691,6 +701,8 @@ defmodule Pleroma.Web.Router do
     get("/web/login", MastodonAPIController, :login)
     delete("/auth/sign_out", MastodonAPIController, :logout)
 
+    post("/auth/password", MastodonAPIController, :password_reset)
+
     scope [] do
       pipe_through(:oauth_read_or_public)
       get("/web/*path", MastodonAPIController, :index)
index e96e4e1e411e489ace59316c5fcebdb8d1e1ab4f..9b01ebcc642ea30acc7f09024bc5f7c9c0fea58b 100644 (file)
@@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do
     {:ok, salmon}
   end
 
-  def remote_users(%{data: %{"to" => to} = data}) do
-    to = to ++ (data["cc"] || [])
+  def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
+    cc = Map.get(data, "cc", [])
+
+    bcc =
+      data
+      |> Map.get("bcc", [])
+      |> Enum.reduce([], fn ap_id, bcc ->
+        case Pleroma.List.get_by_ap_id(ap_id) do
+          %Pleroma.List{user_id: ^user_id} = list ->
+            {:ok, following} = Pleroma.List.get_following(list)
+            bcc ++ Enum.map(following, & &1.ap_id)
+
+          _ ->
+            bcc
+        end
+      end)
 
-    to
-    |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
+    [to, cc, bcc]
+    |> Enum.concat()
+    |> Enum.map(&User.get_cached_by_ap_id/1)
     |> Enum.filter(fn user -> user && !user.local end)
   end
 
@@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do
       {:ok, private, _} = Keys.keys_from_pem(keys)
       {:ok, feed} = encode(private, feed)
 
-      remote_users = remote_users(activity)
+      remote_users = remote_users(user, activity)
 
       salmon_urls = Enum.map(remote_users, & &1.info.salmon)
       reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
index b1863528f36f3b1df42d5b4972905d3e017e9224..9e4da7dca43d587e36a80c6846e8b298d5990809 100644 (file)
@@ -7,10 +7,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
 
   require Logger
 
-  alias Comeonin.Pbkdf2
   alias Pleroma.Activity
+  alias Pleroma.Config
   alias Pleroma.Emoji
+  alias Pleroma.Healthcheck
   alias Pleroma.Notification
+  alias Pleroma.Plugs.AuthenticationPlug
   alias Pleroma.User
   alias Pleroma.Web
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -23,7 +25,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
   end
 
   def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
-    with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do
+    with %User{} = user <- User.get_cached_by_nickname(nick),
+         avatar = User.avatar_url(user) do
       conn
       |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
     else
@@ -96,7 +99,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
     name = followee.nickname
 
     with %User{} = user <- User.get_cached_by_nickname(username),
-         true <- Pbkdf2.checkpw(password, user.password_hash),
+         true <- AuthenticationPlug.checkpw(password, user.password_hash),
          %User{} = _followed <- User.get_cached_by_id(id),
          {:ok, follower} <- User.follow(user, followee),
          {:ok, _activity} <- ActivityPub.follow(follower, followee) do
@@ -338,20 +341,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
   end
 
   def healthcheck(conn, _params) do
-    info =
-      if Pleroma.Config.get([:instance, :healthcheck]) do
-        Pleroma.Healthcheck.system_info()
-      else
-        %{}
-      end
+    with true <- Config.get([:instance, :healthcheck]),
+         %{healthy: true} = info <- Healthcheck.system_info() do
+      json(conn, info)
+    else
+      %{healthy: false} = info ->
+        service_unavailable(conn, info)
 
-    conn =
-      if info[:healthy] do
-        conn
-      else
-        Plug.Conn.put_status(conn, :service_unavailable)
-      end
+      _ ->
+        service_unavailable(conn, %{})
+    end
+  end
 
-    json(conn, info)
+  defp service_unavailable(conn, info) do
+    conn
+    |> put_status(:service_unavailable)
+    |> json(info)
   end
 end
index 41e1c287744b48813b0f1a582b420e4855adf341..bb5dda204d0d1cb168a43af9dc35ca24dfae313d 100644 (file)
@@ -221,6 +221,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       user
       |> UserEmail.password_reset_email(token_record.token)
       |> Mailer.deliver_async()
+
+      {:ok, :enqueued}
     else
       false ->
         {:error, "bad user identifier"}
index 45ef7be3d277bdc14f7468012ad3ad5f28794e92..5dfab6a6c396e2ac3f8694a2d786c36aba09cedb 100644 (file)
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
   require Logger
 
+  plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
   plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
   action_fallback(:errors)
 
@@ -192,6 +193,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   end
 
   def notifications(%{assigns: %{user: user}} = conn, params) do
+    params =
+      if Map.has_key?(params, "with_muted") do
+        Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"])
+      else
+        params
+      end
+
     notifications = Notification.for_user(user, params)
 
     conn
@@ -430,6 +438,12 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
     with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
       json_response(conn, :no_content, "")
+    else
+      {:error, "unknown user"} ->
+        send_resp(conn, :not_found, "")
+
+      {:error, _} ->
+        send_resp(conn, :bad_request, "")
     end
   end
 
index bf09775e6e79ffa383af21597940bed005f237bb..0cc172698f61ab9bf48108ef31d3299c59e84947 100644 (file)
@@ -11,10 +11,6 @@ defmodule Pleroma.Web.UploaderController do
     process_callback(conn, :global.whereis_name({Uploader, upload_path}), params)
   end
 
-  def callbacks(conn, _) do
-    render_error(conn, :bad_request, "bad request")
-  end
-
   defp process_callback(conn, pid, params) when is_pid(pid) do
     send(pid, {Uploader, self(), conn, params})
 
index 3fca72de84f9806eddf5547638a2503a46390fc1..fa34c7ced9e7bff22c735606510ba3a10bfded56 100644 (file)
@@ -32,7 +32,7 @@ defmodule Pleroma.Web.WebFinger do
 
   def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
     host = Pleroma.Web.Endpoint.host()
-    regex = ~r/(acct:)?(?<username>\w+)@#{host}/
+    regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
 
     with %{"username" => username} <- Regex.named_captures(regex, resource),
          %User{} = user <- User.get_cached_by_nickname(username) do
diff --git a/mix.exs b/mix.exs
index f96789d215870752c286ffa2aaa2d03669bf12c9..c12b0a500d33bf924de383b0a5a02fc00ac80c49 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -14,7 +14,7 @@ defmodule Pleroma.Mixfile do
       aliases: aliases(),
       deps: deps(),
       test_coverage: [tool: ExCoveralls],
-
+      preferred_cli_env: ["coveralls.html": :test],
       # Docs
       name: "Pleroma",
       homepage_url: "https://pleroma.social/",
@@ -95,6 +95,7 @@ defmodule Pleroma.Mixfile do
   defp deps do
     [
       {:phoenix, "~> 1.4.8"},
+      {:tzdata, "~> 1.0"},
       {:plug_cowboy, "~> 2.0"},
       {:phoenix_pubsub, "~> 1.1"},
       {:phoenix_ecto, "~> 4.0"},
@@ -137,7 +138,7 @@ defmodule Pleroma.Mixfile do
        ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
       {:http_signatures,
        git: "https://git.pleroma.social/pleroma/http_signatures.git",
-       ref: "9789401987096ead65646b52b5a2ca6bf52fc531"},
+       ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
       {:pleroma_job_queue, "~> 0.2.0"},
       {:telemetry, "~> 0.3"},
       {:prometheus_ex, "~> 3.0"},
index 9c0fd0e983fd08a8b933fc35490da0be0e0ccdc6..45142ba8fd455a961a646c375b1b0e5bdfccfa15 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -6,7 +6,7 @@
   "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
   "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
   "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
-  "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
+  "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
   "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
   "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
   "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
@@ -38,7 +38,7 @@
   "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
   "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
   "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
-  "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "9789401987096ead65646b52b5a2ca6bf52fc531", [ref: "9789401987096ead65646b52b5a2ca6bf52fc531"]},
+  "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
   "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
   "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
@@ -82,9 +82,9 @@
   "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
   "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
   "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
-  "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
+  "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
   "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
-  "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
+  "tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
   "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
   "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
diff --git a/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs b/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs
new file mode 100644 (file)
index 0000000..3c32bc3
--- /dev/null
@@ -0,0 +1,26 @@
+defmodule Pleroma.Repo.Migrations.AddApIdToLists do
+  use Ecto.Migration
+
+  def up do
+    alter table(:lists) do
+      add(:ap_id, :string)
+    end
+
+    execute("""
+    UPDATE lists
+    SET ap_id = u.ap_id || '/lists/' || lists.id
+    FROM users AS u
+    WHERE lists.user_id = u.id
+    """)
+
+    create(unique_index(:lists, :ap_id))
+  end
+
+  def down do
+    drop(index(:lists, [:ap_id]))
+
+    alter table(:lists) do
+      remove(:ap_id)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs
new file mode 100644 (file)
index 0000000..5066990
--- /dev/null
@@ -0,0 +1,24 @@
+defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do
+  use Ecto.Migration
+  alias Pleroma.User
+
+  def change do
+    query =
+      User.Query.build(%{
+        local: true,
+        active: true,
+        order_by: :id
+      })
+
+    Pleroma.Repo.stream(query)
+    |> Enum.each(fn
+      %{info: %{mutes: mutes} = info} = user ->
+        info_cng =
+          Ecto.Changeset.cast(info, %{muted_notifications: mutes}, [:muted_notifications])
+
+        Ecto.Changeset.change(user)
+        |> Ecto.Changeset.put_embed(:info, info_cng)
+        |> Pleroma.Repo.update()
+    end)
+  end
+end
index f36b231c5b070c8654aa766f40f98ff40b416736..57ed05ebaf39315e41bb3b41d260dc161c073191 100644 (file)
             "sensitive": "as:sensitive",
             "litepub": "http://litepub.social/ns#",
             "directMessage": "litepub:directMessage",
+            "listMessage": {
+                "@id": "litepub:listMessage",
+                "@type": "@id"
+            },
             "oauthRegistrationEndpoint": {
                 "@id": "litepub:oauthRegistrationEndpoint",
                 "@type": "@id"
diff --git a/test/fixtures/rich_media/amz.html b/test/fixtures/rich_media/amz.html
new file mode 100644 (file)
index 0000000..d4f8bd1
--- /dev/null
@@ -0,0 +1,5 @@
+<meta name="twitter:card" content="summary" />
+<meta name="twitter:site" content="@flickr" />
+<meta name="twitter:title" content="Small Island Developing States Photo Submission" />
+<meta name="twitter:description" content="View the album on Flickr." />
+<meta name="twitter:image" content="https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20190716T175105Z&X-Amz-Expires=300000&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" />
index 1909c0cd9e664771e7e425eab8542c5e7d8e8a2a..f39033d022d7c58f32ba379ddae54bbc52cdf859 100644 (file)
@@ -113,4 +113,30 @@ defmodule Pleroma.ListTest do
     assert owned_list in lists_2
     refute not_owned_list in lists_2
   end
+
+  test "get by ap_id" do
+    user = insert(:user)
+    {:ok, list} = Pleroma.List.create("foo", user)
+    assert Pleroma.List.get_by_ap_id(list.ap_id) == list
+  end
+
+  test "memberships" do
+    user = insert(:user)
+    member = insert(:user)
+    {:ok, list} = Pleroma.List.create("foo", user)
+    {:ok, list} = Pleroma.List.follow(list, member)
+
+    assert Pleroma.List.memberships(member) == [list.ap_id]
+  end
+
+  test "member?" do
+    user = insert(:user)
+    member = insert(:user)
+
+    {:ok, list} = Pleroma.List.create("foo", user)
+    {:ok, list} = Pleroma.List.follow(list, member)
+
+    assert Pleroma.List.member?(list, member)
+    refute Pleroma.List.member?(list, user)
+  end
 end
index 1d36f14bfcb076de8b113a6371ad6bf606f61f58..dda570b499b5841ba3ebaff7b5cd01d771cba954 100644 (file)
@@ -74,26 +74,37 @@ defmodule Pleroma.NotificationTest do
       Task.await(task_user_notification)
     end
 
-    test "it doesn't create a notification for user if the user blocks the activity author" do
+    test "it creates a notification for user if the user blocks the activity author" do
       activity = insert(:note_activity)
       author = User.get_cached_by_ap_id(activity.data["actor"])
       user = insert(:user)
       {:ok, user} = User.block(user, author)
 
-      refute Notification.create_notification(activity, user)
+      assert Notification.create_notification(activity, user)
     end
 
-    test "it doesn't create a notificatin for the user if the user mutes the activity author" do
+    test "it creates a notificatin for the user if the user mutes the activity author" do
       muter = insert(:user)
       muted = insert(:user)
       {:ok, _} = User.mute(muter, muted)
       muter = Repo.get(User, muter.id)
       {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
 
-      refute Notification.create_notification(activity, muter)
+      assert Notification.create_notification(activity, muter)
     end
 
-    test "it doesn't create a notification for an activity from a muted thread" do
+    test "notification created if user is muted without notifications" do
+      muter = insert(:user)
+      muted = insert(:user)
+
+      {:ok, muter} = User.mute(muter, muted, false)
+
+      {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
+
+      assert Notification.create_notification(activity, muter)
+    end
+
+    test "it creates a notification for an activity from a muted thread" do
       muter = insert(:user)
       other_user = insert(:user)
       {:ok, activity} = CommonAPI.post(muter, %{"status" => "hey"})
@@ -105,7 +116,7 @@ defmodule Pleroma.NotificationTest do
           "in_reply_to_status_id" => activity.id
         })
 
-      refute Notification.create_notification(activity, muter)
+      assert Notification.create_notification(activity, muter)
     end
 
     test "it disables notifications from followers" do
@@ -532,4 +543,98 @@ defmodule Pleroma.NotificationTest do
       assert Enum.empty?(Notification.for_user(user))
     end
   end
+
+  describe "for_user" do
+    test "it returns notifications for muted user without notifications" do
+      user = insert(:user)
+      muted = insert(:user)
+      {:ok, user} = User.mute(user, muted, false)
+
+      {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+
+      assert length(Notification.for_user(user)) == 1
+    end
+
+    test "it doesn't return notifications for muted user with notifications" do
+      user = insert(:user)
+      muted = insert(:user)
+      {:ok, user} = User.mute(user, muted)
+
+      {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+
+      assert Notification.for_user(user) == []
+    end
+
+    test "it doesn't return notifications for blocked user" do
+      user = insert(:user)
+      blocked = insert(:user)
+      {:ok, user} = User.block(user, blocked)
+
+      {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+
+      assert Notification.for_user(user) == []
+    end
+
+    test "it doesn't return notificatitons for blocked domain" do
+      user = insert(:user)
+      blocked = insert(:user, ap_id: "http://some-domain.com")
+      {:ok, user} = User.block_domain(user, "some-domain.com")
+
+      {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+
+      assert Notification.for_user(user) == []
+    end
+
+    test "it doesn't return notifications for muted thread" do
+      user = insert(:user)
+      another_user = insert(:user)
+
+      {:ok, activity} =
+        TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
+
+      {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
+      assert Notification.for_user(user) == []
+    end
+
+    test "it returns notifications for muted user with notifications and with_muted parameter" do
+      user = insert(:user)
+      muted = insert(:user)
+      {:ok, user} = User.mute(user, muted)
+
+      {:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
+
+      assert length(Notification.for_user(user, %{with_muted: true})) == 1
+    end
+
+    test "it returns notifications for blocked user and with_muted parameter" do
+      user = insert(:user)
+      blocked = insert(:user)
+      {:ok, user} = User.block(user, blocked)
+
+      {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+
+      assert length(Notification.for_user(user, %{with_muted: true})) == 1
+    end
+
+    test "it returns notificatitons for blocked domain and with_muted parameter" do
+      user = insert(:user)
+      blocked = insert(:user, ap_id: "http://some-domain.com")
+      {:ok, user} = User.block_domain(user, "some-domain.com")
+
+      {:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
+
+      assert length(Notification.for_user(user, %{with_muted: true})) == 1
+    end
+
+    test "it returns notifications for muted thread with_muted parameter" do
+      user = insert(:user)
+      another_user = insert(:user)
+
+      {:ok, activity} =
+        TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
+
+      {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
+      assert length(Notification.for_user(user, %{with_muted: true})) == 1
+    end
+  end
 end
index 1beed623646936ce43fd3bee76203ee119f2b640..61cd1b41228d896e72f4596789e331ce3e2a2f66 100644 (file)
@@ -68,4 +68,34 @@ defmodule Pleroma.Object.ContainmentTest do
                "[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}"
     end
   end
+
+  describe "containment of children" do
+    test "contain_child() catches spoofing attempts" do
+      data = %{
+        "id" => "http://example.com/whatever",
+        "type" => "Create",
+        "object" => %{
+          "id" => "http://example.net/~alyssa/activities/1234",
+          "attributedTo" => "http://example.org/~alyssa"
+        },
+        "actor" => "http://example.com/~bob"
+      }
+
+      :error = Containment.contain_child(data)
+    end
+
+    test "contain_child() allows correct origins" do
+      data = %{
+        "id" => "http://example.org/~alyssa/activities/5678",
+        "type" => "Create",
+        "object" => %{
+          "id" => "http://example.org/~alyssa/activities/1234",
+          "attributedTo" => "http://example.org/~alyssa"
+        },
+        "actor" => "http://example.org/~alyssa"
+      }
+
+      :ok = Containment.contain_child(data)
+    end
+  end
 end
index 3b666e0d141fc77a8604442c1087b7622041a598..482252cffa6fa10ca7346df8e7fbc77917b331a1 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Object.FetcherTest do
   alias Pleroma.Object
   alias Pleroma.Object.Fetcher
   import Tesla.Mock
+  import Mock
 
   setup do
     mock(fn
@@ -26,16 +27,31 @@ defmodule Pleroma.Object.FetcherTest do
   end
 
   describe "actor origin containment" do
-    test "it rejects objects with a bogus origin" do
+    test_with_mock "it rejects objects with a bogus origin",
+                   Pleroma.Web.OStatus,
+                   [:passthrough],
+                   [] do
       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
+
+      refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
     end
 
-    test "it rejects objects when attributedTo is wrong (variant 1)" do
+    test_with_mock "it rejects objects when attributedTo is wrong (variant 1)",
+                   Pleroma.Web.OStatus,
+                   [:passthrough],
+                   [] do
       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
+
+      refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
     end
 
-    test "it rejects objects when attributedTo is wrong (variant 2)" do
+    test_with_mock "it rejects objects when attributedTo is wrong (variant 2)",
+                   Pleroma.Web.OStatus,
+                   [:passthrough],
+                   [] do
       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
+
+      refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
     end
   end
 
@@ -134,4 +150,34 @@ defmodule Pleroma.Object.FetcherTest do
       assert object.id != object_two.id
     end
   end
+
+  describe "signed fetches" do
+    test_with_mock "it signs fetches when configured to do so",
+                   Pleroma.Signature,
+                   [:passthrough],
+                   [] do
+      option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
+      Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
+
+      Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
+
+      assert called(Pleroma.Signature.sign(:_, :_))
+
+      Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
+    end
+
+    test_with_mock "it doesn't sign fetches when not configured to do so",
+                   Pleroma.Signature,
+                   [:passthrough],
+                   [] do
+      option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
+      Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
+
+      Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
+
+      refute called(Pleroma.Signature.sign(:_, :_))
+
+      Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
+    end
+  end
 end
index 6158086ea212b421ba2d41d3a7fb6de19160942b..7ca04561638e7f1b9dc3e6802dac179e9910ae47 100644 (file)
@@ -8,6 +8,9 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
   alias Pleroma.Plugs.AuthenticationPlug
   alias Pleroma.User
 
+  import ExUnit.CaptureLog
+  import Mock
+
   setup %{conn: conn} do
     user = %User{
       id: 1,
@@ -54,4 +57,32 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
 
     assert conn == ret_conn
   end
+
+  describe "checkpw/2" do
+    test "check pbkdf2 hash" do
+      hash =
+        "$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A"
+
+      assert AuthenticationPlug.checkpw("test-password", hash)
+      refute AuthenticationPlug.checkpw("test-password1", hash)
+    end
+
+    test "check sha512-crypt hash" do
+      hash =
+        "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
+
+      with_mock :crypt, crypt: fn _password, password_hash -> password_hash end do
+        assert AuthenticationPlug.checkpw("password", hash)
+      end
+    end
+
+    test "it returns false when hash invalid" do
+      hash =
+        "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
+
+      assert capture_log(fn ->
+               refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash)
+             end) =~ "[error] Password hash not recognized"
+    end
+  end
 end
index efd811df777dddefb77f9fdc621fe693079a8d0e..d6fd9ea81af8b5cf172da5e0ce5b194c48ffa527 100644 (file)
@@ -26,22 +26,4 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
       assert called(HTTPSignatures.validate_conn(:_))
     end
   end
-
-  test "bails out early if the signature isn't by the activity actor" do
-    params = %{"actor" => "https://mst3k.interlinked.me/users/luciferMysticus"}
-    conn = build_conn(:get, "/doesntmattter", params)
-
-    with_mock HTTPSignatures, validate_conn: fn _ -> false end do
-      conn =
-        conn
-        |> put_req_header(
-          "signature",
-          "keyId=\"http://mastodon.example.org/users/admin#main-key"
-        )
-        |> HTTPSignaturePlug.call(%{})
-
-      assert conn.assigns.valid_signature == false
-      refute called(HTTPSignatures.validate_conn(:_))
-    end
-  end
 end
diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs
new file mode 100644 (file)
index 0000000..bb45d9e
--- /dev/null
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
+  use Pleroma.Web.ConnCase
+  alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
+
+  import Tesla.Mock
+  import Plug.Conn
+
+  setup do
+    mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  defp set_signature(conn, key_id) do
+    conn
+    |> put_req_header("signature", "keyId=\"#{key_id}\"")
+    |> assign(:valid_signature, true)
+  end
+
+  test "it successfully maps a valid identity with a valid signature" do
+    conn =
+      build_conn(:get, "/doesntmattter")
+      |> set_signature("http://mastodon.example.org/users/admin")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    refute is_nil(conn.assigns.user)
+  end
+
+  test "it successfully maps a valid identity with a valid signature with payload" do
+    conn =
+      build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+      |> set_signature("http://mastodon.example.org/users/admin")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    refute is_nil(conn.assigns.user)
+  end
+
+  test "it considers a mapped identity to be invalid when it mismatches a payload" do
+    conn =
+      build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+      |> set_signature("https://niu.moe/users/rye")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    assert %{valid_signature: false} == conn.assigns
+  end
+
+  @tag skip: "known breakage; the testsuite presently depends on it"
+  test "it considers a mapped identity to be invalid when the identity cannot be found" do
+    conn =
+      build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+      |> set_signature("http://niu.moe/users/rye")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    assert %{valid_signature: false} == conn.assigns
+  end
+end
index f8251b5c78ff09c47c1148fd6eeb6ed30da439bb..395095079f1346b377af6e251a2ebab54cb3fdea 100644 (file)
@@ -10,12 +10,13 @@ defmodule Pleroma.Plugs.RateLimiterTest do
 
   import Pleroma.Factory
 
-  @limiter_name :testing
+  # Note: each example must work with separate buckets in order to prevent concurrency issues
 
   test "init/1" do
-    Pleroma.Config.put([:rate_limit, @limiter_name], {1, 1})
+    limiter_name = :test_init
+    Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
 
-    assert {@limiter_name, {1, 1}} == RateLimiter.init(@limiter_name)
+    assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name)
     assert nil == RateLimiter.init(:foo)
   end
 
@@ -24,14 +25,15 @@ defmodule Pleroma.Plugs.RateLimiterTest do
   end
 
   test "it restricts by opts" do
+    limiter_name = :test_opts
     scale = 1000
     limit = 5
 
-    Pleroma.Config.put([:rate_limit, @limiter_name], {scale, limit})
+    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
 
-    opts = RateLimiter.init(@limiter_name)
+    opts = RateLimiter.init(limiter_name)
     conn = conn(:get, "/")
-    bucket_name = "#{@limiter_name}:#{RateLimiter.ip(conn)}"
+    bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
 
     conn = RateLimiter.call(conn, opts)
     assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
@@ -65,18 +67,78 @@ defmodule Pleroma.Plugs.RateLimiterTest do
     refute conn.halted
   end
 
+  test "`bucket_name` option overrides default bucket name" do
+    limiter_name = :test_bucket_name
+    scale = 1000
+    limit = 5
+
+    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+    base_bucket_name = "#{limiter_name}:group1"
+    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
+
+    conn = conn(:get, "/")
+    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+    customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
+
+    RateLimiter.call(conn, opts)
+    assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit)
+    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+  end
+
+  test "`params` option appends specified params' values to bucket name" do
+    limiter_name = :test_params
+    scale = 1000
+    limit = 5
+
+    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+    opts = RateLimiter.init({limiter_name, params: ["id"]})
+    id = "1"
+
+    conn = conn(:get, "/?id=#{id}")
+    conn = Plug.Conn.fetch_query_params(conn)
+
+    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+    parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
+
+    RateLimiter.call(conn, opts)
+    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
+    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+  end
+
+  test "it supports combination of options modifying bucket name" do
+    limiter_name = :test_options_combo
+    scale = 1000
+    limit = 5
+
+    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+    base_bucket_name = "#{limiter_name}:group1"
+    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]})
+    id = "100"
+
+    conn = conn(:get, "/?id=#{id}")
+    conn = Plug.Conn.fetch_query_params(conn)
+
+    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+    parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}"
+
+    RateLimiter.call(conn, opts)
+    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
+    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+  end
+
   test "optional limits for authenticated users" do
+    limiter_name = :test_authenticated
     Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
 
     scale = 1000
     limit = 5
-    Pleroma.Config.put([:rate_limit, @limiter_name], [{1, 10}, {scale, limit}])
+    Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
 
-    opts = RateLimiter.init(@limiter_name)
+    opts = RateLimiter.init(limiter_name)
 
     user = insert(:user)
     conn = conn(:get, "/") |> assign(:user, user)
-    bucket_name = "#{@limiter_name}:#{user.id}"
+    bucket_name = "#{limiter_name}:#{user.id}"
 
     conn = RateLimiter.call(conn, opts)
     assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
index f542de97ce3f0f130b0314eaa750dbe99adcf9d1..f4b7d6add825133a4ce59c7318c83fd4404b6848 100644 (file)
@@ -5,7 +5,6 @@
 defmodule Pleroma.ReverseProxyTest do
   use Pleroma.Web.ConnCase, async: true
   import ExUnit.CaptureLog
-  import ExUnit.CaptureLog
   import Mox
   alias Pleroma.ReverseProxy
   alias Pleroma.ReverseProxy.ClientMock
diff --git a/test/signature_test.exs b/test/signature_test.exs
new file mode 100644 (file)
index 0000000..7400cae
--- /dev/null
@@ -0,0 +1,108 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.SignatureTest do
+  use Pleroma.DataCase
+
+  import ExUnit.CaptureLog
+  import Pleroma.Factory
+  import Tesla.Mock
+
+  alias Pleroma.Signature
+
+  setup do
+    mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----"
+
+  @public_key %{
+    "id" => "https://mastodon.social/users/lambadalambda#main-key",
+    "owner" => "https://mastodon.social/users/lambadalambda",
+    "publicKeyPem" =>
+      "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n"
+  }
+
+  @rsa_public_key {
+    :RSAPublicKey,
+    24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857,
+    # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
+    65_537
+  }
+
+  defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\""
+
+  defp make_fake_conn(key_id),
+    do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
+
+  describe "fetch_public_key/1" do
+    test "it returns key" do
+      expected_result = {:ok, @rsa_public_key}
+
+      user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}})
+
+      assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
+    end
+
+    test "it returns error when not found user" do
+      assert capture_log(fn ->
+               assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) ==
+                        {:error, :error}
+             end) =~ "[error] Could not decode user"
+    end
+
+    test "it returns error if public key is empty" do
+      user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}})
+
+      assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) ==
+               {:error, :error}
+    end
+  end
+
+  describe "refetch_public_key/1" do
+    test "it returns key" do
+      ap_id = "https://mastodon.social/users/lambadalambda"
+
+      assert Signature.refetch_public_key(make_fake_conn(ap_id)) ==
+               {:ok, @rsa_public_key}
+    end
+
+    test "it returns error when not found user" do
+      assert capture_log(fn ->
+               assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) ==
+                        {:error, {:error, :ok}}
+             end) =~ "[error] Could not decode user"
+    end
+  end
+
+  describe "sign/2" do
+    test "it returns signature headers" do
+      user =
+        insert(:user, %{
+          ap_id: "https://mastodon.social/users/lambadalambda",
+          info: %{keys: @private_key}
+        })
+
+      assert Signature.sign(
+               user,
+               %{
+                 host: "test.test",
+                 "content-length": 100
+               }
+             ) ==
+               "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
+    end
+
+    test "it returns error" do
+      user =
+        insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", info: %{keys: ""}})
+
+      assert Signature.sign(
+               user,
+               %{host: "test.test", "content-length": 100}
+             ) == {:error, []}
+    end
+  end
+end
index df260bd3f9794dbd9d6f7470505cec5cbaf4eaf8..f3d98e7e3173f46bf19bbd7b2c3235eaa2b0291d 100644 (file)
@@ -42,19 +42,18 @@ defmodule Pleroma.DataCase do
     :ok
   end
 
-  def ensure_local_uploader(_context) do
+  def ensure_local_uploader(context) do
+    test_uploader = Map.get(context, :uploader, Pleroma.Uploaders.Local)
     uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
     filters = Pleroma.Config.get([Pleroma.Upload, :filters])
 
-    unless uploader == Pleroma.Uploaders.Local || filters != [] do
-      Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
-      Pleroma.Config.put([Pleroma.Upload, :filters], [])
+    Pleroma.Config.put([Pleroma.Upload, :uploader], test_uploader)
+    Pleroma.Config.put([Pleroma.Upload, :filters], [])
 
-      on_exit(fn ->
-        Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
-        Pleroma.Config.put([Pleroma.Upload, :filters], filters)
-      end)
-    end
+    on_exit(fn ->
+      Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
+      Pleroma.Config.put([Pleroma.Upload, :filters], filters)
+    end)
 
     :ok
   end
index ff6bb78f9f8c0e8919eeb438242a37af5aabe01a..7811f78074e22a403e8ea291d7c915e1b7d93267 100644 (file)
@@ -879,6 +879,42 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("https://info.pleroma.site/activity.json", _, _, Accept: "application/activity+json") do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity.json")
+     }}
+  end
+
+  def get("https://info.pleroma.site/activity.json", _, _, _) do
+    {:ok, %Tesla.Env{status: 404, body: ""}}
+  end
+
+  def get("https://info.pleroma.site/activity2.json", _, _, Accept: "application/activity+json") do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity2.json")
+     }}
+  end
+
+  def get("https://info.pleroma.site/activity2.json", _, _, _) do
+    {:ok, %Tesla.Env{status: 404, body: ""}}
+  end
+
+  def get("https://info.pleroma.site/activity3.json", _, _, Accept: "application/activity+json") do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json")
+     }}
+  end
+
+  def get("https://info.pleroma.site/activity3.json", _, _, _) do
+    {:ok, %Tesla.Env{status: 404, body: ""}}
+  end
+
   def get(url, query, body, headers) do
     {:error,
      "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{
index bbcc5721719a7de635ad95130d8f7d53df49ea3b..a9b79eb5b572a8bcf44fa1a137063059c7c807b8 100644 (file)
@@ -34,8 +34,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
 
     Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
 
-    first_db = Config.get_by_params(%{group: "pleroma", key: "first_setting"})
-    second_db = Config.get_by_params(%{group: "pleroma", key: "second_setting"})
+    first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"})
+    second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"})
     refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"})
 
     assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]]
@@ -45,13 +45,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
   test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
     Config.create(%{
       group: "pleroma",
-      key: "setting_first",
+      key: ":setting_first",
       value: [key: "value", key2: [Pleroma.Activity]]
     })
 
     Config.create(%{
       group: "pleroma",
-      key: "setting_second",
+      key: ":setting_second",
       value: [key: "valu2", key2: [Pleroma.Repo]]
     })
 
@@ -61,7 +61,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
     assert File.exists?(temp_file)
     {:ok, file} = File.read(temp_file)
 
-    assert file =~ "config :pleroma, setting_first:"
-    assert file =~ "config :pleroma, setting_second:"
+    assert file =~ "config :pleroma, :setting_first,"
+    assert file =~ "config :pleroma, :setting_second,"
   end
 end
diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs
new file mode 100644 (file)
index 0000000..fddd594
--- /dev/null
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.DedupeTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Upload
+  alias Pleroma.Upload.Filter.Dedupe
+
+  @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781"
+
+  test "adds shasum" do
+    File.cp!(
+      "test/fixtures/image.jpg",
+      "test/fixtures/image_tmp.jpg"
+    )
+
+    upload = %Upload{
+      name: "an… image.jpg",
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image_tmp.jpg"),
+      tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+    }
+
+    assert {
+             :ok,
+             %Pleroma.Upload{id: @shasum, path: "#{@shasum}.jpg"}
+           } = Dedupe.filter(upload)
+  end
+end
diff --git a/test/upload/filter/mogrifun_test.exs b/test/upload/filter/mogrifun_test.exs
new file mode 100644 (file)
index 0000000..d5a8751
--- /dev/null
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.MogrifunTest do
+  use Pleroma.DataCase
+  import Mock
+
+  alias Pleroma.Upload
+  alias Pleroma.Upload.Filter
+
+  test "apply mogrify filter" do
+    File.cp!(
+      "test/fixtures/image.jpg",
+      "test/fixtures/image_tmp.jpg"
+    )
+
+    upload = %Upload{
+      name: "an… image.jpg",
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image_tmp.jpg"),
+      tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+    }
+
+    task =
+      Task.async(fn ->
+        assert_receive {:apply_filter, {}}, 4_000
+      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
+    end
+
+    Task.await(task)
+  end
+end
diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs
new file mode 100644 (file)
index 0000000..c301440
--- /dev/null
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.MogrifyTest do
+  use Pleroma.DataCase
+  import Mock
+
+  alias Pleroma.Config
+  alias Pleroma.Upload
+  alias Pleroma.Upload.Filter
+
+  setup do
+    filter = Config.get([Filter.Mogrify, :args])
+
+    on_exit(fn ->
+      Config.put([Filter.Mogrify, :args], filter)
+    end)
+  end
+
+  test "apply mogrify filter" do
+    Config.put([Filter.Mogrify, :args], [{"tint", "40"}])
+
+    File.cp!(
+      "test/fixtures/image.jpg",
+      "test/fixtures/image_tmp.jpg"
+    )
+
+    upload = %Upload{
+      name: "an… image.jpg",
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image_tmp.jpg"),
+      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
+    end
+
+    Task.await(task)
+  end
+end
diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs
new file mode 100644 (file)
index 0000000..640cd71
--- /dev/null
@@ -0,0 +1,39 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.FilterTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Config
+  alias Pleroma.Upload.Filter
+
+  setup do
+    custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text])
+
+    on_exit(fn ->
+      Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename)
+    end)
+  end
+
+  test "applies filters" do
+    Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
+
+    File.cp!(
+      "test/fixtures/image.jpg",
+      "test/fixtures/image_tmp.jpg"
+    )
+
+    upload = %Pleroma.Upload{
+      name: "an… image.jpg",
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image_tmp.jpg"),
+      tempfile: Path.absname("test/fixtures/image_tmp.jpg")
+    }
+
+    assert Filter.filter([], upload) == {:ok, upload}
+
+    assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload)
+    assert upload.name == "custom-file.png"
+  end
+end
index 946ebcb5aa54e512395b2067d15b1598acb02c53..32c6977d1a05c5a4c5590142941bc3f209b1dda9 100644 (file)
@@ -3,9 +3,106 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.UploadTest do
-  alias Pleroma.Upload
   use Pleroma.DataCase
 
+  import ExUnit.CaptureLog
+
+  alias Pleroma.Upload
+  alias Pleroma.Uploaders.Uploader
+
+  @upload_file %Plug.Upload{
+    content_type: "image/jpg",
+    path: Path.absname("test/fixtures/image_tmp.jpg"),
+    filename: "image.jpg"
+  }
+
+  defmodule TestUploaderBase do
+    def put_file(%{path: path} = _upload, module_name) do
+      task_pid =
+        Task.async(fn ->
+          :timer.sleep(10)
+
+          {Uploader, path}
+          |> :global.whereis_name()
+          |> send({Uploader, self(), {:test}, %{}})
+
+          assert_receive {Uploader, {:test}}, 4_000
+        end)
+
+      Agent.start(fn -> task_pid end, name: module_name)
+
+      :wait_callback
+    end
+  end
+
+  describe "Tried storing a file when http callback response success result" do
+    defmodule TestUploaderSuccess do
+      def http_callback(conn, _params),
+        do: {:ok, conn, {:file, "post-process-file.jpg"}}
+
+      def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__)
+    end
+
+    setup do: [uploader: TestUploaderSuccess]
+    setup [:ensure_local_uploader]
+
+    test "it returns file" do
+      File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+      assert Upload.store(@upload_file) ==
+               {:ok,
+                %{
+                  "name" => "image.jpg",
+                  "type" => "Document",
+                  "url" => [
+                    %{
+                      "href" => "http://localhost:4001/media/post-process-file.jpg",
+                      "mediaType" => "image/jpeg",
+                      "type" => "Link"
+                    }
+                  ]
+                }}
+
+      Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end))
+    end
+  end
+
+  describe "Tried storing a file when http callback response error" do
+    defmodule TestUploaderError do
+      def http_callback(conn, _params), do: {:error, conn, "Errors"}
+
+      def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__)
+    end
+
+    setup do: [uploader: TestUploaderError]
+    setup [:ensure_local_uploader]
+
+    test "it returns error" do
+      File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+      assert capture_log(fn ->
+               assert Upload.store(@upload_file) == {:error, "Errors"}
+               Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end))
+             end) =~
+               "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\""
+    end
+  end
+
+  describe "Tried storing a file when http callback doesn't response by timeout" do
+    defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback))
+    setup do: [uploader: TestUploader]
+    setup [:ensure_local_uploader]
+
+    test "it returns error" do
+      File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+      assert capture_log(fn ->
+               assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"}
+             end) =~
+               "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\""
+    end
+  end
+
   describe "Storing a file with the Local uploader" do
     setup [:ensure_local_uploader]
 
index 7c3fe976d5d53a831e9c58e5f4e223a1f0ca4ca0..908f72a0ea08ebf48b46e7e591e2d86a5070fed4 100644 (file)
@@ -687,10 +687,12 @@ defmodule Pleroma.UserTest do
       muted_user = insert(:user)
 
       refute User.mutes?(user, muted_user)
+      refute User.muted_notifications?(user, muted_user)
 
       {:ok, user} = User.mute(user, muted_user)
 
       assert User.mutes?(user, muted_user)
+      assert User.muted_notifications?(user, muted_user)
     end
 
     test "it unmutes users" do
@@ -701,6 +703,20 @@ defmodule Pleroma.UserTest do
       {:ok, user} = User.unmute(user, muted_user)
 
       refute User.mutes?(user, muted_user)
+      refute User.muted_notifications?(user, muted_user)
+    end
+
+    test "it mutes user without notifications" do
+      user = insert(:user)
+      muted_user = insert(:user)
+
+      refute User.mutes?(user, muted_user)
+      refute User.muted_notifications?(user, muted_user)
+
+      {:ok, user} = User.mute(user, muted_user, false)
+
+      assert User.mutes?(user, muted_user)
+      refute User.muted_notifications?(user, muted_user)
     end
   end
 
@@ -1294,4 +1310,21 @@ defmodule Pleroma.UserTest do
       assert following == 0
     end
   end
+
+  describe "is_internal_user?/1" do
+    test "non-internal user returns false" do
+      user = insert(:user)
+      refute User.is_internal_user?(user)
+    end
+
+    test "user with no nickname returns true" do
+      user = insert(:user, %{nickname: nil})
+      assert User.is_internal_user?(user)
+    end
+
+    test "user with internal-prefixed nickname returns true" do
+      user = insert(:user, %{nickname: "internal.test"})
+      assert User.is_internal_user?(user)
+    end
+  end
 end
index 452172bb49bbbbcc2fd4390bcb4f686c06ba33f3..40344f17ea8c076fdb57a6fdea4c1b5f0ca39829 100644 (file)
@@ -48,6 +48,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
     end
   end
 
+  describe "/internal/fetch" do
+    test "it returns the internal fetch user", %{conn: conn} do
+      res =
+        conn
+        |> get(activity_pub_path(conn, :internal_fetch))
+        |> json_response(200)
+
+      assert res["id"] =~ "/fetch"
+    end
+  end
+
   describe "/users/:nickname" do
     test "it returns a json representation of the user with accept application/json", %{
       conn: conn
index 24d8493fe1a399fea5ef6af8c227b8fe82a11b62..0370a1799d1c94972e23ed218e77966158b174cf 100644 (file)
@@ -1190,6 +1190,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
+  test "fetch_activities/2 returns activities addressed to a list " do
+    user = insert(:user)
+    member = insert(:user)
+    {:ok, list} = Pleroma.List.create("foo", user)
+    {:ok, list} = Pleroma.List.follow(list, member)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
+
+    activity = Repo.preload(activity, :bookmark)
+    activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
+
+    assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
+  end
+
   def data_uri do
     File.read!("test/fixtures/avatar_data_uri")
   end
diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs
new file mode 100644 (file)
index 0000000..9fd9c31
--- /dev/null
@@ -0,0 +1,92 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.MRF.MentionPolicy
+
+  test "pass filter if allow list is empty" do
+    Pleroma.Config.delete([:mrf_mention])
+
+    message = %{
+      "type" => "Create",
+      "to" => ["https://example.com/ok"],
+      "cc" => ["https://example.com/blocked"]
+    }
+
+    assert MentionPolicy.filter(message) == {:ok, message}
+  end
+
+  describe "allow" do
+    test "empty" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create"
+      }
+
+      assert MentionPolicy.filter(message) == {:ok, message}
+    end
+
+    test "to" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create",
+        "to" => ["https://example.com/ok"]
+      }
+
+      assert MentionPolicy.filter(message) == {:ok, message}
+    end
+
+    test "cc" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create",
+        "cc" => ["https://example.com/ok"]
+      }
+
+      assert MentionPolicy.filter(message) == {:ok, message}
+    end
+
+    test "both" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create",
+        "to" => ["https://example.com/ok"],
+        "cc" => ["https://example.com/ok2"]
+      }
+
+      assert MentionPolicy.filter(message) == {:ok, message}
+    end
+  end
+
+  describe "deny" do
+    test "to" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create",
+        "to" => ["https://example.com/blocked"]
+      }
+
+      assert MentionPolicy.filter(message) == {:reject, nil}
+    end
+
+    test "cc" do
+      Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]})
+
+      message = %{
+        "type" => "Create",
+        "to" => ["https://example.com/ok"],
+        "cc" => ["https://example.com/blocked"]
+      }
+
+      assert MentionPolicy.filter(message) == {:reject, nil}
+    end
+  end
+end
index 6d05138fbf3a81667444a1b38f879e66ab3e3fab..e7498e00570cf59d6541b8e1d01b8027671213f8 100644 (file)
@@ -416,6 +416,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         |> Map.put("attributedTo", user.ap_id)
         |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
         |> Map.put("cc", [])
+        |> Map.put("id", user.ap_id <> "/activities/12345678")
 
       data = Map.put(data, "object", object)
 
@@ -439,6 +440,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         |> Map.put("attributedTo", user.ap_id)
         |> Map.put("to", nil)
         |> Map.put("cc", nil)
+        |> Map.put("id", user.ap_id <> "/activities/12345678")
 
       data = Map.put(data, "object", object)
 
@@ -1096,6 +1098,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 
       assert modified["directMessage"] == true
     end
+
+    test "it strips BCC field" do
+      user = insert(:user)
+      {:ok, list} = Pleroma.List.create("foo", user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
+
+      {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+
+      assert is_nil(modified["bcc"])
+    end
   end
 
   describe "user upgrade" do
index 4d5c07da42a8d8bac425f44dfcbbb147aa8f655a..b62a89e688712028950185ee525fb2a0234049f0 100644 (file)
@@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     following = insert(:user)
     unrelated = insert(:user)
     {:ok, following} = Pleroma.User.follow(following, user)
+    {:ok, list} = Pleroma.List.create("foo", user)
+
+    Pleroma.List.follow(list, unrelated)
 
     {:ok, public} =
       CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"})
@@ -29,6 +32,12 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     {:ok, unlisted} =
       CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"})
 
+    {:ok, list} =
+      CommonAPI.post(user, %{
+        "status" => "@#{mentioned.nickname}",
+        "visibility" => "list:#{list.id}"
+      })
+
     %{
       public: public,
       private: private,
@@ -37,29 +46,65 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
       user: user,
       mentioned: mentioned,
       following: following,
-      unrelated: unrelated
+      unrelated: unrelated,
+      list: list
     }
   end
 
-  test "is_direct?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
+  test "is_direct?", %{
+    public: public,
+    private: private,
+    direct: direct,
+    unlisted: unlisted,
+    list: list
+  } do
     assert Visibility.is_direct?(direct)
     refute Visibility.is_direct?(public)
     refute Visibility.is_direct?(private)
     refute Visibility.is_direct?(unlisted)
+    assert Visibility.is_direct?(list)
   end
 
-  test "is_public?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
+  test "is_public?", %{
+    public: public,
+    private: private,
+    direct: direct,
+    unlisted: unlisted,
+    list: list
+  } do
     refute Visibility.is_public?(direct)
     assert Visibility.is_public?(public)
     refute Visibility.is_public?(private)
     assert Visibility.is_public?(unlisted)
+    refute Visibility.is_public?(list)
   end
 
-  test "is_private?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
+  test "is_private?", %{
+    public: public,
+    private: private,
+    direct: direct,
+    unlisted: unlisted,
+    list: list
+  } do
     refute Visibility.is_private?(direct)
     refute Visibility.is_private?(public)
     assert Visibility.is_private?(private)
     refute Visibility.is_private?(unlisted)
+    refute Visibility.is_private?(list)
+  end
+
+  test "is_list?", %{
+    public: public,
+    private: private,
+    direct: direct,
+    unlisted: unlisted,
+    list: list
+  } do
+    refute Visibility.is_list?(direct)
+    refute Visibility.is_list?(public)
+    refute Visibility.is_list?(private)
+    refute Visibility.is_list?(unlisted)
+    assert Visibility.is_list?(list)
   end
 
   test "visible_for_user?", %{
@@ -70,7 +115,8 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     user: user,
     mentioned: mentioned,
     following: following,
-    unrelated: unrelated
+    unrelated: unrelated,
+    list: list
   } do
     # All visible to author
 
@@ -78,6 +124,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     assert Visibility.visible_for_user?(private, user)
     assert Visibility.visible_for_user?(unlisted, user)
     assert Visibility.visible_for_user?(direct, user)
+    assert Visibility.visible_for_user?(list, user)
 
     # All visible to a mentioned user
 
@@ -85,6 +132,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     assert Visibility.visible_for_user?(private, mentioned)
     assert Visibility.visible_for_user?(unlisted, mentioned)
     assert Visibility.visible_for_user?(direct, mentioned)
+    assert Visibility.visible_for_user?(list, mentioned)
 
     # DM not visible for just follower
 
@@ -92,6 +140,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     assert Visibility.visible_for_user?(private, following)
     assert Visibility.visible_for_user?(unlisted, following)
     refute Visibility.visible_for_user?(direct, following)
+    refute Visibility.visible_for_user?(list, following)
 
     # Public and unlisted visible for unrelated user
 
@@ -99,6 +148,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     assert Visibility.visible_for_user?(unlisted, unrelated)
     refute Visibility.visible_for_user?(private, unrelated)
     refute Visibility.visible_for_user?(direct, unrelated)
+
+    # Visible for a list member
+    assert Visibility.visible_for_user?(list, unrelated)
   end
 
   test "doesn't die when the user doesn't exist",
@@ -115,18 +167,24 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     public: public,
     private: private,
     direct: direct,
-    unlisted: unlisted
+    unlisted: unlisted,
+    list: list
   } do
     assert Visibility.get_visibility(public) == "public"
     assert Visibility.get_visibility(private) == "private"
     assert Visibility.get_visibility(direct) == "direct"
     assert Visibility.get_visibility(unlisted) == "unlisted"
+    assert Visibility.get_visibility(list) == "list"
   end
 
   test "get_visibility with directMessage flag" do
     assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
   end
 
+  test "get_visibility with listMessage flag" do
+    assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list"
+  end
+
   describe "entire_thread_visible_for_user?/2" do
     test "returns false if not found activity", %{user: user} do
       refute Visibility.entire_thread_visible_for_user?(%Activity{}, user)
index 1b71cbff384835fb033489f71d90b41b452c82c8..ee48b752c29e631157baa7d11803e32058d6a3cb 100644 (file)
@@ -1720,7 +1720,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
           configs: [
             %{
               "group" => "pleroma",
-              "key" => "key1",
+              "key" => ":key1",
               "value" => [
                 %{"tuple" => [":key2", "some_val"]},
                 %{
@@ -1750,7 +1750,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                  "configs" => [
                    %{
                      "group" => "pleroma",
-                     "key" => "key1",
+                     "key" => ":key1",
                      "value" => [
                        %{"tuple" => [":key2", "some_val"]},
                        %{
@@ -1782,7 +1782,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
           configs: [
             %{
               "group" => "pleroma",
-              "key" => "key1",
+              "key" => ":key1",
               "value" => %{"key" => "some_val"}
             }
           ]
@@ -1793,7 +1793,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                  "configs" => [
                    %{
                      "group" => "pleroma",
-                     "key" => "key1",
+                     "key" => ":key1",
                      "value" => %{"key" => "some_val"}
                    }
                  ]
@@ -1862,6 +1862,45 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                ]
              }
     end
+
+    test "queues key as atom", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{
+              "group" => "pleroma_job_queue",
+              "key" => ":queues",
+              "value" => [
+                %{"tuple" => [":federator_incoming", 50]},
+                %{"tuple" => [":federator_outgoing", 50]},
+                %{"tuple" => [":web_push", 50]},
+                %{"tuple" => [":mailer", 10]},
+                %{"tuple" => [":transmogrifier", 20]},
+                %{"tuple" => [":scheduled_activities", 10]},
+                %{"tuple" => [":background", 5]}
+              ]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => "pleroma_job_queue",
+                   "key" => ":queues",
+                   "value" => [
+                     %{"tuple" => [":federator_incoming", 50]},
+                     %{"tuple" => [":federator_outgoing", 50]},
+                     %{"tuple" => [":web_push", 50]},
+                     %{"tuple" => [":mailer", 10]},
+                     %{"tuple" => [":transmogrifier", 20]},
+                     %{"tuple" => [":scheduled_activities", 10]},
+                     %{"tuple" => [":background", 5]}
+                   ]
+                 }
+               ]
+             }
+    end
   end
 end
 
index 958c931c49a21353d6cdf5f9ac8a286ec1f3c1e0..16b3f121d52caf83eb245e7635f394b2e687cf16 100644 (file)
@@ -129,6 +129,37 @@ defmodule Pleroma.Web.CommonAPITest do
                  })
       end)
     end
+
+    test "it allows to address a list" do
+      user = insert(:user)
+      {:ok, list} = Pleroma.List.create("foo", user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
+
+      assert activity.data["bcc"] == [list.ap_id]
+      assert activity.recipients == [list.ap_id, user.ap_id]
+      assert activity.data["listMessage"] == list.ap_id
+    end
+
+    test "it returns error when status is empty and no attachments" do
+      user = insert(:user)
+
+      assert {:error, "Cannot post an empty status without attachments"} =
+               CommonAPI.post(user, %{"status" => ""})
+    end
+
+    test "it returns error when character limit is exceeded" do
+      limit = Pleroma.Config.get([:instance, :limit])
+      Pleroma.Config.put([:instance, :limit], 5)
+
+      user = insert(:user)
+
+      assert {:error, "The status is over the character limit"} =
+               CommonAPI.post(user, %{"status" => "foobar"})
+
+      Pleroma.Config.put([:instance, :limit], limit)
+    end
   end
 
   describe "reactions" do
@@ -346,6 +377,20 @@ defmodule Pleroma.Web.CommonAPITest do
     end
   end
 
+  describe "unfollow/2" do
+    test "also unsubscribes a user" do
+      [follower, followed] = insert_pair(:user)
+      {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
+      {:ok, followed} = User.subscribe(follower, followed)
+
+      assert User.subscribed_to?(follower, followed)
+
+      {:ok, follower} = CommonAPI.unfollow(follower, followed)
+
+      refute User.subscribed_to?(follower, followed)
+    end
+  end
+
   describe "accept_follow_request/2" do
     test "after acceptance, it sets all existing pending follow request states to 'accept'" do
       user = insert(:user, info: %{locked: true})
@@ -387,4 +432,23 @@ defmodule Pleroma.Web.CommonAPITest do
       assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
     end
   end
+
+  describe "vote/3" do
+    test "does not allow to vote twice" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Am I cute?",
+          "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+        })
+
+      object = Object.normalize(activity)
+
+      {:ok, _, object} = CommonAPI.vote(other_user, object, [0])
+
+      assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
+    end
+  end
 end
index b3a334d50ed8c25a317d6a4a08d44d1d67af2520..af320f31f3700929f9d1e51e27b153fb88f26a16 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
   alias Pleroma.Web.Endpoint
   use Pleroma.DataCase
 
+  import ExUnit.CaptureLog
   import Pleroma.Factory
 
   @public_address "https://www.w3.org/ns/activitystreams#Public"
@@ -202,7 +203,9 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
 
       expected = ""
 
-      assert Utils.date_to_asctime(date) == expected
+      assert capture_log(fn ->
+               assert Utils.date_to_asctime(date) == expected
+             end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601"
     end
 
     test "when date is a Unix timestamp" do
@@ -210,13 +213,23 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
 
       expected = ""
 
-      assert Utils.date_to_asctime(date) == expected
+      assert capture_log(fn ->
+               assert Utils.date_to_asctime(date) == expected
+             end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601"
     end
 
     test "when date is nil" do
       expected = ""
 
-      assert Utils.date_to_asctime(nil) == expected
+      assert capture_log(fn ->
+               assert Utils.date_to_asctime(nil) == expected
+             end) =~ "[warn] Date  in wrong format, must be ISO 8601"
+    end
+
+    test "when date is a random string" do
+      assert capture_log(fn ->
+               assert Utils.date_to_asctime("foo") == ""
+             end) =~ "[warn] Date foo in wrong format, must be ISO 8601"
     end
   end
 
index de6aeec720a9fc229830ed02a48e8bd4b708fdee..fa44d35ccfda02e9f71a66702c88b39fb009ef7d 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
   use Pleroma.DataCase
   import Pleroma.Factory
   alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.AccountView
 
   test "Represent a user account" do
@@ -152,6 +153,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     assert expected == AccountView.render("account.json", %{user: user})
   end
 
+  test "Represent a deactivated user for an admin" do
+    admin = insert(:user, %{info: %{is_admin: true}})
+    deactivated_user = insert(:user, %{info: %{deactivated: true}})
+    represented = AccountView.render("account.json", %{user: deactivated_user, for: admin})
+    assert represented[:pleroma][:deactivated] == true
+  end
+
   test "Represent a smaller mention" do
     user = insert(:user)
 
@@ -165,28 +173,90 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     assert expected == AccountView.render("mention.json", %{user: user})
   end
 
-  test "represent a relationship" do
-    user = insert(:user)
-    other_user = insert(:user)
+  describe "relationship" do
+    test "represent a relationship for the following and followed user" do
+      user = insert(:user)
+      other_user = insert(:user)
 
-    {:ok, user} = User.follow(user, other_user)
-    {:ok, user} = User.block(user, other_user)
+      {:ok, user} = User.follow(user, other_user)
+      {:ok, other_user} = User.follow(other_user, user)
+      {:ok, other_user} = User.subscribe(user, other_user)
+      {:ok, user} = User.mute(user, other_user, true)
+      {:ok, user} = CommonAPI.hide_reblogs(user, other_user)
 
-    expected = %{
-      id: to_string(other_user.id),
-      following: false,
-      followed_by: false,
-      blocking: true,
-      muting: false,
-      muting_notifications: false,
-      subscribing: false,
-      requested: false,
-      domain_blocking: false,
-      showing_reblogs: true,
-      endorsed: false
-    }
+      expected = %{
+        id: to_string(other_user.id),
+        following: true,
+        followed_by: true,
+        blocking: false,
+        blocked_by: false,
+        muting: true,
+        muting_notifications: true,
+        subscribing: true,
+        requested: false,
+        domain_blocking: false,
+        showing_reblogs: false,
+        endorsed: false
+      }
+
+      assert expected ==
+               AccountView.render("relationship.json", %{user: user, target: other_user})
+    end
+
+    test "represent a relationship for the blocking and blocked user" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, user} = User.follow(user, other_user)
+      {:ok, other_user} = User.subscribe(user, other_user)
+      {:ok, user} = User.block(user, other_user)
+      {:ok, other_user} = User.block(other_user, user)
+
+      expected = %{
+        id: to_string(other_user.id),
+        following: false,
+        followed_by: false,
+        blocking: true,
+        blocked_by: true,
+        muting: false,
+        muting_notifications: false,
+        subscribing: false,
+        requested: false,
+        domain_blocking: false,
+        showing_reblogs: true,
+        endorsed: false
+      }
+
+      assert expected ==
+               AccountView.render("relationship.json", %{user: user, target: other_user})
+    end
+
+    test "represent a relationship for the user with a pending follow request" do
+      user = insert(:user)
+      other_user = insert(:user, %{info: %User.Info{locked: true}})
+
+      {:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
+      user = User.get_cached_by_id(user.id)
+      other_user = User.get_cached_by_id(other_user.id)
+
+      expected = %{
+        id: to_string(other_user.id),
+        following: false,
+        followed_by: false,
+        blocking: false,
+        blocked_by: false,
+        muting: false,
+        muting_notifications: false,
+        subscribing: false,
+        requested: true,
+        domain_blocking: false,
+        showing_reblogs: true,
+        endorsed: false
+      }
 
-    assert expected == AccountView.render("relationship.json", %{user: user, target: other_user})
+      assert expected ==
+               AccountView.render("relationship.json", %{user: user, target: other_user})
+    end
   end
 
   test "represent an embedded relationship" do
@@ -240,6 +310,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
           following: false,
           followed_by: false,
           blocking: true,
+          blocked_by: false,
           subscribing: false,
           muting: false,
           muting_notifications: false,
index b7c050dbf30d6b235ede5b7ab0f056f2b6f6ff9c..b4b1dd785cbf3a755a442b000ef044c5c98f8436 100644 (file)
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   import Pleroma.Factory
   import ExUnit.CaptureLog
   import Tesla.Mock
+  import Swoosh.TestAssertions
 
   @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
 
@@ -35,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     user = insert(:user)
     following = insert(:user)
 
-    {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
+    {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
 
     conn =
       conn
@@ -58,7 +59,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     following = insert(:user)
 
     capture_log(fn ->
-      {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
+      {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
 
       {:ok, [_activity]} =
         OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
@@ -1004,8 +1005,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     test "list timeline", %{conn: conn} do
       user = insert(:user)
       other_user = insert(:user)
-      {:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."})
-      {:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
+      {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
+      {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
       {:ok, list} = Pleroma.List.create("name", user)
       {:ok, list} = Pleroma.List.follow(list, other_user)
 
@@ -1022,10 +1023,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
       user = insert(:user)
       other_user = insert(:user)
-      {:ok, activity_one} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
+      {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
 
       {:ok, _activity_two} =
-        TwitterAPI.create_status(other_user, %{
+        CommonAPI.post(other_user, %{
           "status" => "Marisa is cute.",
           "visibility" => "private"
         })
@@ -1049,8 +1050,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       user = insert(:user)
       other_user = insert(:user)
 
-      {:ok, activity} =
-        TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
 
       {:ok, [_notification]} = Notification.create_notifications(activity)
 
@@ -1072,8 +1072,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       user = insert(:user)
       other_user = insert(:user)
 
-      {:ok, activity} =
-        TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
 
       {:ok, [notification]} = Notification.create_notifications(activity)
 
@@ -1095,8 +1094,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       user = insert(:user)
       other_user = insert(:user)
 
-      {:ok, activity} =
-        TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
 
       {:ok, [notification]} = Notification.create_notifications(activity)
 
@@ -1112,8 +1110,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       user = insert(:user)
       other_user = insert(:user)
 
-      {:ok, activity} =
-        TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
 
       {:ok, [_notification]} = Notification.create_notifications(activity)
 
@@ -1274,6 +1271,71 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       result = json_response(conn_res, 200)
       assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
     end
+
+    test "doesn't see notifications after muting user with notifications", %{conn: conn} do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, _, _, _} = CommonAPI.follow(user, user2)
+      {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+      conn = assign(conn, :user, user)
+
+      conn = get(conn, "/api/v1/notifications")
+
+      assert length(json_response(conn, 200)) == 1
+
+      {:ok, user} = User.mute(user, user2)
+
+      conn = assign(build_conn(), :user, user)
+      conn = get(conn, "/api/v1/notifications")
+
+      assert json_response(conn, 200) == []
+    end
+
+    test "see notifications after muting user without notifications", %{conn: conn} do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, _, _, _} = CommonAPI.follow(user, user2)
+      {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+      conn = assign(conn, :user, user)
+
+      conn = get(conn, "/api/v1/notifications")
+
+      assert length(json_response(conn, 200)) == 1
+
+      {:ok, user} = User.mute(user, user2, false)
+
+      conn = assign(build_conn(), :user, user)
+      conn = get(conn, "/api/v1/notifications")
+
+      assert length(json_response(conn, 200)) == 1
+    end
+
+    test "see notifications after muting user with notifications and with_muted parameter", %{
+      conn: conn
+    } do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, _, _, _} = CommonAPI.follow(user, user2)
+      {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"})
+
+      conn = assign(conn, :user, user)
+
+      conn = get(conn, "/api/v1/notifications")
+
+      assert length(json_response(conn, 200)) == 1
+
+      {:ok, user} = User.mute(user, user2)
+
+      conn = assign(build_conn(), :user, user)
+      conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
+
+      assert length(json_response(conn, 200)) == 1
+    end
   end
 
   describe "reblogging" do
@@ -1330,6 +1392,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
       assert to_string(activity.id) == id
     end
+
+    test "returns 400 error when activity is not exist", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/foo/reblog")
+
+      assert json_response(conn, 400) == %{"error" => "Could not repeat"}
+    end
   end
 
   describe "unreblogging" do
@@ -1348,6 +1421,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
       assert to_string(activity.id) == id
     end
+
+    test "returns 400 error when activity is not exist", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/foo/unreblog")
+
+      assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
+    end
   end
 
   describe "favoriting" do
@@ -1366,16 +1450,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert to_string(activity.id) == id
     end
 
-    test "returns 500 for a wrong id", %{conn: conn} do
+    test "returns 400 error for a wrong id", %{conn: conn} do
       user = insert(:user)
 
-      resp =
+      conn =
         conn
         |> assign(:user, user)
         |> post("/api/v1/statuses/1/favourite")
-        |> json_response(500)
 
-      assert resp == "Something went wrong"
+      assert json_response(conn, 400) == %{"error" => "Could not favorite"}
     end
   end
 
@@ -1396,6 +1479,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
       assert to_string(activity.id) == id
     end
+
+    test "returns 400 error for a wrong id", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/1/unfavourite")
+
+      assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
+    end
   end
 
   describe "user timelines" do
@@ -1466,10 +1560,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
       media =
         TwitterAPI.upload(file, user, "json")
-        |> Poison.decode!()
+        |> Jason.decode!()
 
       {:ok, image_post} =
-        TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
+        CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
 
       conn =
         conn
@@ -1792,7 +1886,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     following = insert(:user)
 
     capture_log(fn ->
-      {:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
+      {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
 
       {:ok, [_activity]} =
         OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
@@ -2105,25 +2199,52 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
   end
 
-  test "muting / unmuting a user", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
+  describe "mute/unmute" do
+    test "with notifications", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
 
-    conn =
-      conn
-      |> assign(:user, user)
-      |> post("/api/v1/accounts/#{other_user.id}/mute")
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/accounts/#{other_user.id}/mute")
 
-    assert %{"id" => _id, "muting" => true} = json_response(conn, 200)
+      response = json_response(conn, 200)
 
-    user = User.get_cached_by_id(user.id)
+      assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
+      user = User.get_cached_by_id(user.id)
 
-    conn =
-      build_conn()
-      |> assign(:user, user)
-      |> post("/api/v1/accounts/#{other_user.id}/unmute")
+      conn =
+        build_conn()
+        |> assign(:user, user)
+        |> post("/api/v1/accounts/#{other_user.id}/unmute")
+
+      response = json_response(conn, 200)
+      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+    end
+
+    test "without notifications", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
+
+      response = json_response(conn, 200)
 
-    assert %{"id" => _id, "muting" => false} = json_response(conn, 200)
+      assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
+      user = User.get_cached_by_id(user.id)
+
+      conn =
+        build_conn()
+        |> assign(:user, user)
+        |> post("/api/v1/accounts/#{other_user.id}/unmute")
+
+      response = json_response(conn, 200)
+      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+    end
   end
 
   test "subscribing / unsubscribing to a user", %{conn: conn} do
@@ -2524,7 +2645,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     insert(:user, %{local: false, nickname: "u@peer1.com"})
     insert(:user, %{local: false, nickname: "u@peer2.com"})
 
-    {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
+    {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
 
     # Stats should count users with missing or nil `info.deactivated` value
     user = User.get_cached_by_id(user.id)
@@ -2617,6 +2738,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                |> json_response(200)
     end
 
+    test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
+      {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/#{dm.id}/pin")
+
+      assert json_response(conn, 400) == %{"error" => "Could not pin"}
+    end
+
     test "unpin status", %{conn: conn, user: user, activity: activity} do
       {:ok, _} = CommonAPI.pin(activity.id, user)
 
@@ -2636,6 +2768,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                |> json_response(200)
     end
 
+    test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/1/unpin")
+
+      assert json_response(conn, 400) == %{"error" => "Could not unpin"}
+    end
+
     test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
       {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
 
@@ -2810,6 +2951,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                |> json_response(200)
     end
 
+    test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
+      {:ok, _} = CommonAPI.add_mute(user, activity)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses/#{activity.id}/mute")
+
+      assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
+    end
+
     test "unmute conversation", %{conn: conn, user: user, activity: activity} do
       {:ok, _} = CommonAPI.add_mute(user, activity)
 
@@ -2854,7 +3006,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                |> post("/api/v1/reports", %{
                  "account_id" => target_user.id,
                  "status_ids" => [activity.id],
-                 "comment" => "bad status!"
+                 "comment" => "bad status!",
+                 "forward" => "false"
                })
                |> json_response(200)
     end
@@ -2887,6 +3040,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
                |> json_response(400)
     end
+
+    test "returns error when account is not exist", %{
+      conn: conn,
+      reporter: reporter,
+      activity: activity
+    } do
+      conn =
+        conn
+        |> assign(:user, reporter)
+        |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
+
+      assert json_response(conn, 400) == %{"error" => "Account not found"}
+    end
   end
 
   describe "link headers" do
@@ -3246,7 +3412,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     user2 = insert(:user)
     user3 = insert(:user)
 
-    {:ok, replied_to} = TwitterAPI.create_status(user1, %{"status" => "cofe"})
+    {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
 
     # Reply to status from another user
     conn1 =
@@ -3411,7 +3577,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
         |> get("/api/v1/polls/#{object.id}")
 
       response = json_response(conn, 200)
-      id = object.id
+      id = to_string(object.id)
       assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
     end
 
@@ -3511,5 +3677,186 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
                total_items == 1
              end)
     end
+
+    test "does not allow choice index to be greater than options count", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Am I cute?",
+          "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+        })
+
+      object = Object.normalize(activity)
+
+      conn =
+        conn
+        |> assign(:user, other_user)
+        |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
+
+      assert json_response(conn, 422) == %{"error" => "Invalid indices"}
+    end
+
+    test "returns 404 error when object is not exist", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
+
+      assert json_response(conn, 404) == %{"error" => "Record not found"}
+    end
+
+    test "returns 404 when poll is private and not available for user", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Am I cute?",
+          "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
+          "visibility" => "private"
+        })
+
+      object = Object.normalize(activity)
+
+      conn =
+        conn
+        |> assign(:user, other_user)
+        |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
+
+      assert json_response(conn, 404) == %{"error" => "Record not found"}
+    end
+  end
+
+  describe "GET /api/v1/statuses/:id/favourited_by" do
+    setup do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+
+      conn =
+        build_conn()
+        |> assign(:user, user)
+
+      [conn: conn, activity: activity]
+    end
+
+    test "returns users who have favorited the status", %{conn: conn, activity: activity} do
+      other_user = insert(:user)
+      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+
+      response =
+        conn
+        |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+        |> json_response(:ok)
+
+      [%{"id" => id}] = response
+
+      assert id == other_user.id
+    end
+
+    test "returns empty array when status has not been favorited yet", %{
+      conn: conn,
+      activity: activity
+    } do
+      response =
+        conn
+        |> get("/api/v1/statuses/#{activity.id}/favourited_by")
+        |> json_response(:ok)
+
+      assert Enum.empty?(response)
+    end
+  end
+
+  describe "GET /api/v1/statuses/:id/reblogged_by" do
+    setup do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+
+      conn =
+        build_conn()
+        |> assign(:user, user)
+
+      [conn: conn, activity: activity]
+    end
+
+    test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
+      other_user = insert(:user)
+      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
+
+      response =
+        conn
+        |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+        |> json_response(:ok)
+
+      [%{"id" => id}] = response
+
+      assert id == other_user.id
+    end
+
+    test "returns empty array when status has not been reblogged yet", %{
+      conn: conn,
+      activity: activity
+    } do
+      response =
+        conn
+        |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
+        |> json_response(:ok)
+
+      assert Enum.empty?(response)
+    end
+  end
+
+  describe "POST /auth/password, with valid parameters" do
+    setup %{conn: conn} do
+      user = insert(:user)
+      conn = post(conn, "/auth/password?email=#{user.email}")
+      %{conn: conn, user: user}
+    end
+
+    test "it returns 204", %{conn: conn} do
+      assert json_response(conn, :no_content)
+    end
+
+    test "it creates a PasswordResetToken record for user", %{user: user} do
+      token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+      assert token_record
+    end
+
+    test "it sends an email to user", %{user: user} do
+      token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+
+      email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
+      notify_email = Pleroma.Config.get([:instance, :notify_email])
+      instance_name = Pleroma.Config.get([:instance, :name])
+
+      assert_email_sent(
+        from: {instance_name, notify_email},
+        to: {user.name, user.email},
+        html_body: email.html_body
+      )
+    end
+  end
+
+  describe "POST /auth/password, with invalid parameters" do
+    setup do
+      user = insert(:user)
+      {:ok, user: user}
+    end
+
+    test "it returns 404 when user is not found", %{conn: conn, user: user} do
+      conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
+      assert conn.status == 404
+      assert conn.resp_body == ""
+    end
+
+    test "it returns 400 when user is not local", %{conn: conn, user: user} do
+      {:ok, user} = Repo.update(Changeset.change(user, local: false))
+      conn = post(conn, "/auth/password?email=#{user.email}")
+      assert conn.status == 400
+      assert conn.resp_body == ""
+    end
   end
 end
index 9f50c09f49fbb1d03898fac33fbbd614ef9a3dad..043b96c1413d2c425f21e136d04b0a50fb2c84b6 100644 (file)
@@ -24,12 +24,16 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
         {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
         {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
       ] do
-        conn = get(conn, "/api/v2/search", %{"q" => "2hu"})
-
-        assert results = json_response(conn, 200)
-
-        assert results["accounts"] == []
-        assert results["statuses"] == []
+        capture_log(fn ->
+          results =
+            conn
+            |> get("/api/v2/search", %{"q" => "2hu"})
+            |> json_response(200)
+
+          assert results["accounts"] == []
+          assert results["statuses"] == []
+        end) =~
+          "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
       end
     end
 
@@ -99,14 +103,16 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
         {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
         {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
       ] do
-        conn =
-          conn
-          |> get("/api/v1/search", %{"q" => "2hu"})
-
-        assert results = json_response(conn, 200)
-
-        assert results["accounts"] == []
-        assert results["statuses"] == []
+        capture_log(fn ->
+          results =
+            conn
+            |> get("/api/v1/search", %{"q" => "2hu"})
+            |> json_response(200)
+
+          assert results["accounts"] == []
+          assert results["statuses"] == []
+        end) =~
+          "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
       end
     end
 
index ac42819d8e4a20ac278f8aff10d88b1f1b554885..3447c5b1f1309c11f745f7e4bb2f1ef79518d67b 100644 (file)
@@ -423,7 +423,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
       expected = %{
         emojis: [],
         expired: false,
-        id: object.id,
+        id: to_string(object.id),
         multiple: false,
         options: [
           %{title: "absolutely!", votes_count: 0},
@@ -541,4 +541,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
     assert result[:reblog][:account][:pleroma][:relationship] ==
              AccountView.render("relationship.json", %{user: user, target: user})
   end
+
+  test "visibility/list" do
+    user = insert(:user)
+
+    {:ok, list} = Pleroma.List.create("foo", user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
+
+    status = StatusView.render("status.json", activity: activity)
+
+    assert status.visibility == "list"
+  end
 end
diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs
new file mode 100644 (file)
index 0000000..53b8f55
--- /dev/null
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
+  use Pleroma.Web.ConnCase
+  import Mock
+  alias Pleroma.Config
+
+  setup do
+    media_proxy_config = Config.get([:media_proxy]) || []
+    on_exit(fn -> Config.put([:media_proxy], media_proxy_config) end)
+    :ok
+  end
+
+  test "it returns 404 when MediaProxy disabled", %{conn: conn} do
+    Config.put([:media_proxy, :enabled], false)
+
+    assert %Plug.Conn{
+             status: 404,
+             resp_body: "Not Found"
+           } = get(conn, "/proxy/hhgfh/eeeee")
+
+    assert %Plug.Conn{
+             status: 404,
+             resp_body: "Not Found"
+           } = get(conn, "/proxy/hhgfh/eeee/fff")
+  end
+
+  test "it returns 403 when signature invalidated", %{conn: conn} do
+    Config.put([:media_proxy, :enabled], true)
+    Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
+    path = URI.parse(Pleroma.Web.MediaProxy.encode_url("https://google.fn")).path
+    Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000")
+
+    assert %Plug.Conn{
+             status: 403,
+             resp_body: "Forbidden"
+           } = get(conn, path)
+
+    assert %Plug.Conn{
+             status: 403,
+             resp_body: "Forbidden"
+           } = get(conn, "/proxy/hhgfh/eeee")
+
+    assert %Plug.Conn{
+             status: 403,
+             resp_body: "Forbidden"
+           } = get(conn, "/proxy/hhgfh/eeee/fff")
+  end
+
+  test "redirects on valid url when filename invalidated", %{conn: conn} do
+    Config.put([:media_proxy, :enabled], true)
+    Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
+    url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png")
+    invalid_url = String.replace(url, "test.png", "test-file.png")
+    response = get(conn, invalid_url)
+    html = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>"
+    assert response.status == 302
+    assert response.resp_body == html
+  end
+
+  test "it performs ReverseProxy.call when signature valid", %{conn: conn} do
+    Config.put([:media_proxy, :enabled], true)
+    Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
+    url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png")
+
+    with_mock Pleroma.ReverseProxy,
+      call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do
+      assert %Plug.Conn{status: :success} = get(conn, url)
+    end
+  end
+end
similarity index 87%
rename from test/media_proxy_test.exs
rename to test/web/media_proxy/media_proxy_test.exs
index fbf2009310382a3f4cd70da33b0b5166006689a2..edbbf9b667aa5dbfdf2c8a1a592e7f25a7db5cfd 100644 (file)
@@ -2,7 +2,7 @@
 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.MediaProxyTest do
+defmodule Pleroma.Web.MediaProxyTest do
   use ExUnit.Case
   import Pleroma.Web.MediaProxy
   alias Pleroma.Web.MediaProxy.MediaProxyController
@@ -90,22 +90,39 @@ defmodule Pleroma.MediaProxyTest do
 
     test "filename_matches preserves the encoded or decoded path" do
       assert MediaProxyController.filename_matches(
-               true,
+               %{"filename" => "/Hello world.jpg"},
                "/Hello world.jpg",
                "http://pleroma.social/Hello world.jpg"
              ) == :ok
 
       assert MediaProxyController.filename_matches(
-               true,
+               %{"filename" => "/Hello%20world.jpg"},
                "/Hello%20world.jpg",
                "http://pleroma.social/Hello%20world.jpg"
              ) == :ok
 
       assert MediaProxyController.filename_matches(
-               true,
+               %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"},
                "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
                "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
              ) == :ok
+
+      assert MediaProxyController.filename_matches(
+               %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"},
+               "/my%2Flong%2Furl%2F2019%2F07%2FS.jp",
+               "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
+             ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
+    end
+
+    test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do
+      # conn.request_path will return encoded url
+      request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg"
+
+      assert MediaProxyController.filename_matches(
+               true,
+               request_path,
+               "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg"
+             ) == :ok
     end
 
     test "uses the configured base_url" do
index be11735135f6cac391e9b5538e8c0853694225b2..d7f848bfa7b95857382855dbb7c7123b787eb30c 100644 (file)
@@ -83,4 +83,47 @@ defmodule Pleroma.Web.NodeInfoTest do
 
     Pleroma.Config.put([:instance, :safe_dm_mentions], option)
   end
+
+  test "it shows MRF transparency data if enabled", %{conn: conn} do
+    option = Pleroma.Config.get([:instance, :mrf_transparency])
+    Pleroma.Config.put([:instance, :mrf_transparency], true)
+
+    simple_config = %{"reject" => ["example.com"]}
+    Pleroma.Config.put(:mrf_simple, simple_config)
+
+    response =
+      conn
+      |> get("/nodeinfo/2.1.json")
+      |> json_response(:ok)
+
+    assert response["metadata"]["federation"]["mrf_simple"] == simple_config
+
+    Pleroma.Config.put([:instance, :mrf_transparency], option)
+    Pleroma.Config.put(:mrf_simple, %{})
+  end
+
+  test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do
+    option = Pleroma.Config.get([:instance, :mrf_transparency])
+    Pleroma.Config.put([:instance, :mrf_transparency], true)
+
+    exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
+    Pleroma.Config.put([:instance, :mrf_transparency_exclusions], ["other.site"])
+
+    simple_config = %{"reject" => ["example.com", "other.site"]}
+    expected_config = %{"reject" => ["example.com"]}
+
+    Pleroma.Config.put(:mrf_simple, simple_config)
+
+    response =
+      conn
+      |> get("/nodeinfo/2.1.json")
+      |> json_response(:ok)
+
+    assert response["metadata"]["federation"]["mrf_simple"] == expected_config
+    assert response["metadata"]["federation"]["exclusions"] == true
+
+    Pleroma.Config.put([:instance, :mrf_transparency], option)
+    Pleroma.Config.put([:instance, :mrf_transparency_exclusions], exclusions)
+    Pleroma.Config.put(:mrf_simple, %{})
+  end
 end
index 3dd8c6491fc5fb9a473bfc43a351a3d853572111..bb7648bddea9da399d37ead01b7f793511f43de3 100644 (file)
@@ -4,7 +4,10 @@
 
 defmodule Pleroma.Web.OStatus.OStatusControllerTest do
   use Pleroma.Web.ConnCase
+
+  import ExUnit.CaptureLog
   import Pleroma.Factory
+
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
@@ -27,24 +30,28 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       user = insert(:user)
       salmon = File.read!("test/fixtures/salmon.xml")
 
-      conn =
-        conn
-        |> put_req_header("content-type", "application/atom+xml")
-        |> post("/users/#{user.nickname}/salmon", salmon)
+      assert capture_log(fn ->
+               conn =
+                 conn
+                 |> put_req_header("content-type", "application/atom+xml")
+                 |> post("/users/#{user.nickname}/salmon", salmon)
 
-      assert response(conn, 200)
+               assert response(conn, 200)
+             end) =~ "[error]"
     end
 
     test "decodes a salmon with a changed magic key", %{conn: conn} do
       user = insert(:user)
       salmon = File.read!("test/fixtures/salmon.xml")
 
-      conn =
-        conn
-        |> put_req_header("content-type", "application/atom+xml")
-        |> post("/users/#{user.nickname}/salmon", salmon)
+      assert capture_log(fn ->
+               conn =
+                 conn
+                 |> put_req_header("content-type", "application/atom+xml")
+                 |> post("/users/#{user.nickname}/salmon", salmon)
 
-      assert response(conn, 200)
+               assert response(conn, 200)
+             end) =~ "[error]"
 
       # Set a wrong magic-key for a user so it has to refetch
       salmon_user = User.get_cached_by_ap_id("http://gs.example.org:4040/index.php/user/1")
@@ -61,12 +68,14 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       |> Ecto.Changeset.put_embed(:info, info_cng)
       |> User.update_and_set_cache()
 
-      conn =
-        build_conn()
-        |> put_req_header("content-type", "application/atom+xml")
-        |> post("/users/#{user.nickname}/salmon", salmon)
+      assert capture_log(fn ->
+               conn =
+                 build_conn()
+                 |> put_req_header("content-type", "application/atom+xml")
+                 |> post("/users/#{user.nickname}/salmon", salmon)
 
-      assert response(conn, 200)
+               assert response(conn, 200)
+             end) =~ "[error]"
     end
   end
 
diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs
new file mode 100644 (file)
index 0000000..122787b
--- /dev/null
@@ -0,0 +1,81 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do
+  use ExUnit.Case, async: true
+
+  test "s3 signed url is parsed correct for expiration time" do
+    url = "https://pleroma.social/amz"
+
+    {:ok, timestamp} =
+      Timex.now()
+      |> DateTime.truncate(:second)
+      |> Timex.format("{ISO:Basic:Z}")
+
+    # in seconds
+    valid_till = 30
+
+    metadata = construct_metadata(timestamp, valid_till, url)
+
+    expire_time =
+      Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till)
+
+    assert expire_time == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url)
+  end
+
+  test "s3 signed url is parsed and correct ttl is set for rich media" do
+    url = "https://pleroma.social/amz"
+
+    {:ok, timestamp} =
+      Timex.now()
+      |> DateTime.truncate(:second)
+      |> Timex.format("{ISO:Basic:Z}")
+
+    # in seconds
+    valid_till = 30
+
+    metadata = construct_metadata(timestamp, valid_till, url)
+
+    body = """
+    <meta name="twitter:card" content="Pleroma" />
+    <meta name="twitter:site" content="Pleroma" />
+    <meta name="twitter:title" content="Pleroma" />
+    <meta name="twitter:description" content="Pleroma" />
+    <meta name="twitter:image" content="#{Map.get(metadata, :image)}" />
+    """
+
+    Tesla.Mock.mock(fn
+      %{
+        method: :get,
+        url: "https://pleroma.social/amz"
+      } ->
+        %Tesla.Env{status: 200, body: body}
+    end)
+
+    Cachex.put(:rich_media_cache, url, metadata)
+
+    Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image({:ok, metadata}, url)
+
+    {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url)
+
+    # as there is delay in setting and pulling the data from cache we ignore 1 second
+    assert_in_delta(valid_till * 1000, cache_ttl, 1000)
+  end
+
+  defp construct_s3_url(timestamp, valid_till) do
+    "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{
+      timestamp
+    }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host"
+  end
+
+  defp construct_metadata(timestamp, valid_till, url) do
+    %{
+      image: construct_s3_url(timestamp, valid_till),
+      site: "Pleroma",
+      title: "Pleroma",
+      description: "Pleroma",
+      url: url
+    }
+  end
+end
index 7ec0e101d52b4ba771a384f40f3792a6a6a285e1..8bb8aa36d9d5eca967119e83f5a7f933a93bd475 100644 (file)
@@ -521,6 +521,38 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
                  for: current_user
                })
     end
+
+    test "muted user", %{conn: conn, user: current_user} do
+      other_user = insert(:user)
+
+      {:ok, current_user} = User.mute(current_user, other_user)
+
+      {:ok, _activity} =
+        ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
+
+      conn =
+        conn
+        |> with_credentials(current_user.nickname, "test")
+        |> get("/api/qvitter/statuses/notifications.json")
+
+      assert json_response(conn, 200) == []
+    end
+
+    test "muted user with with_muted parameter", %{conn: conn, user: current_user} do
+      other_user = insert(:user)
+
+      {:ok, current_user} = User.mute(current_user, other_user)
+
+      {:ok, _activity} =
+        ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
+
+      conn =
+        conn
+        |> with_credentials(current_user.nickname, "test")
+        |> get("/api/qvitter/statuses/notifications.json", %{"with_muted" => "true"})
+
+      assert length(json_response(conn, 200)) == 1
+    end
   end
 
   describe "POST /api/qvitter/statuses/notifications/read" do
@@ -1084,15 +1116,17 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
   describe "POST /api/account/password_reset, with invalid parameters" do
     setup [:valid_user]
 
-    test "it returns 500 when user is not found", %{conn: conn, user: user} do
+    test "it returns 404 when user is not found", %{conn: conn, user: user} do
       conn = post(conn, "/api/account/password_reset?email=nonexisting_#{user.email}")
-      assert json_response(conn, :internal_server_error)
+      assert conn.status == 404
+      assert conn.resp_body == ""
     end
 
-    test "it returns 500 when user is not local", %{conn: conn, user: user} do
+    test "it returns 400 when user is not local", %{conn: conn, user: user} do
       {:ok, user} = Repo.update(Changeset.change(user, local: false))
       conn = post(conn, "/api/account/password_reset?email=#{user.email}")
-      assert json_response(conn, :internal_server_error)
+      assert conn.status == 400
+      assert conn.resp_body == ""
     end
   end
 
index 21324399fb3fc5b12863dae94ff152994b5ff796..3d699e1df7a16e606f03bf44e0242b16a444f38b 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
   import Pleroma.Factory
+  import Mock
 
   setup do
     Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -231,10 +232,67 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
     end
   end
 
-  test "GET /api/pleroma/healthcheck", %{conn: conn} do
-    conn = get(conn, "/api/pleroma/healthcheck")
+  describe "GET /api/pleroma/healthcheck" do
+    setup do
+      config_healthcheck = Pleroma.Config.get([:instance, :healthcheck])
 
-    assert conn.status in [200, 503]
+      on_exit(fn ->
+        Pleroma.Config.put([:instance, :healthcheck], config_healthcheck)
+      end)
+
+      :ok
+    end
+
+    test "returns 503 when healthcheck disabled", %{conn: conn} do
+      Pleroma.Config.put([:instance, :healthcheck], false)
+
+      response =
+        conn
+        |> get("/api/pleroma/healthcheck")
+        |> json_response(503)
+
+      assert response == %{}
+    end
+
+    test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
+      Pleroma.Config.put([:instance, :healthcheck], true)
+
+      with_mock Pleroma.Healthcheck,
+        system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
+        response =
+          conn
+          |> get("/api/pleroma/healthcheck")
+          |> json_response(200)
+
+        assert %{
+                 "active" => _,
+                 "healthy" => true,
+                 "idle" => _,
+                 "memory_used" => _,
+                 "pool_size" => _
+               } = response
+      end
+    end
+
+    test "returns 503 when healthcheck enabled and  health is false", %{conn: conn} do
+      Pleroma.Config.put([:instance, :healthcheck], true)
+
+      with_mock Pleroma.Healthcheck,
+        system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do
+        response =
+          conn
+          |> get("/api/pleroma/healthcheck")
+          |> json_response(503)
+
+        assert %{
+                 "active" => _,
+                 "healthy" => false,
+                 "idle" => _,
+                 "memory_used" => _,
+                 "pool_size" => _
+               } = response
+      end
+    end
   end
 
   describe "POST /api/pleroma/disable_account" do
diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs
new file mode 100644 (file)
index 0000000..70028df
--- /dev/null
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.UploaderControllerTest do
+  use Pleroma.Web.ConnCase
+  alias Pleroma.Uploaders.Uploader
+
+  describe "callback/2" do
+    test "it returns 400 response when process callback isn't alive", %{conn: conn} do
+      res =
+        conn
+        |> post(uploader_path(conn, :callback, "test-path"))
+
+      assert res.status == 400
+      assert res.resp_body == "{\"error\":\"bad request\"}"
+    end
+
+    test "it returns success result", %{conn: conn} do
+      task =
+        Task.async(fn ->
+          receive do
+            {Uploader, pid, conn, _params} ->
+              conn =
+                conn
+                |> put_status(:ok)
+                |> Phoenix.Controller.json(%{upload_path: "test-path"})
+
+              send(pid, {Uploader, conn})
+          end
+        end)
+
+      :global.register_name({Uploader, "test-path"}, task.pid)
+
+      res =
+        conn
+        |> post(uploader_path(conn, :callback, "test-path"))
+        |> json_response(200)
+
+      assert res == %{"upload_path" => "test-path"}
+    end
+  end
+end