diff --git a/CC-BY-NC-ND-4.0 b/CC-BY-NC-ND-4.0 deleted file mode 100644 index 486544290..000000000 --- a/CC-BY-NC-ND-4.0 +++ /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. - diff --git a/CHANGELOG.md b/CHANGELOG.md index 942733ab6..86c90da0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,32 +8,45 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config - Configuration: OpenGraph and TwitterCard providers enabled by default - 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 ### 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. +- 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. +- Mastodon API: Add support for muting/unmuting notifications - 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. +- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. +- Addressable lists ### 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. + +## [1.0.1] - 2019-07-14 +### Security +- OStatus: fix an object spoofing vulnerability. ## [1.0.0] - 2019-06-29 ### Security diff --git a/config/config.exs b/config/config.exs index 99b500993..7d539f994 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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, @@ -336,7 +339,12 @@ 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 + ] config :pleroma, :media_proxy, enabled: false, @@ -519,7 +527,9 @@ 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}, + statuses_actions: {10_000, 15}, + status_id_action: {60_000, 3} # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 2cbe1458d..d2e9bcc4b 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -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` @@ -46,14 +48,6 @@ Has these additional fields under the `pleroma` object: - `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` -### Extensions for PleromaFE - -These endpoints added for controlling PleromaFE features over the Mastodon API - -- PATCH `/api/v1/accounts/update_avatar`: Set/clear user avatar image -- PATCH `/api/v1/accounts/update_banner`: Set/clear user banner image -- PATCH `/api/v1/accounts/update_background`: Set/clear user background image - ### Source Has these additional fields under the `pleroma` object: @@ -80,6 +74,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` diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index edc62727a..d83ebd734 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -238,6 +238,13 @@ See [Admin-API](Admin-API.md) ] ``` +## `/api/v1/pleroma/accounts/update_*` +### Set and clear account avatar, banner, and background + +- PATCH `/api/v1/pleroma/accounts/update_avatar`: Set/clear user avatar image +- PATCH `/api/v1/pleroma/accounts/update_banner`: Set/clear user banner image +- PATCH `/api/v1/pleroma/accounts/update_background`: Set/clear user background image + ## `/api/v1/pleroma/mascot` ### Gets user mascot image * Method `GET` diff --git a/docs/config.md b/docs/config.md index 140789d87..9a64f0ed7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -106,6 +106,7 @@ config :pleroma, Pleroma.Emails.Mailer, * `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. @@ -424,6 +425,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 +642,10 @@ 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 +* `: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 diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a71bcd447..a7d0fac5d 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -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 diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 3c13a0558..7799b2a78 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -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) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index c216cdcb1..a1460d303 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -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) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index c96ee7353..dec24458a 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -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 """ diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex index b7bc7a4da..6dd31d3bd 100644 --- a/lib/pleroma/keys.ex +++ b/lib/pleroma/keys.ex @@ -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 diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a5b1cad68..1d320206e 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -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 diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index a414afbbf..ee7b37aab 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -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.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, diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index ada9da0bb..f077a9f32 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -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 diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 101c21f96..96b34ae9f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -32,33 +32,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 diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex index c5e0957e8..31388f574 100644 --- a/lib/pleroma/plugs/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -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 check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do - ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit) + 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(conn, {limiter_name, [{scale, limit} | _]}) do - ExRated.check_rate("#{limiter_name}:#{ip(conn)}", 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 - check_rate(conn, {limiter_name, [{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}, opts}) do + check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) end def ip(%{remote_ip: remote_ip}) do diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index bf31e9cba..1f98f215c 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -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() diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e5a6c2529..ffba3f390 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -749,10 +749,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) @@ -762,9 +765,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) @@ -860,6 +865,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 @@ -1179,10 +1190,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 @@ -1368,23 +1381,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 diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 08e43ff0f..9beb3ddbd 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -24,6 +24,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) @@ -120,6 +121,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} @@ -136,14 +147,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 diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a3174a787..31397b09f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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: diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index cf5176201..e2af4ad1a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -103,43 +103,57 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def following(conn, %{"nickname" => nickname, "page" => page}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_follows, true} <- + {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user, page: page})) + |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) + else + {:show_follows, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def following(conn, %{"nickname" => nickname}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user})) + |> json(UserView.render("following.json", %{user: user, for: for_user})) end end - def followers(conn, %{"nickname" => nickname, "page" => page}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_followers, true} <- + {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user, page: page})) + |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) + else + {:show_followers, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def followers(conn, %{"nickname" => nickname}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user})) + |> json(UserView.render("followers.json", %{user: user, for: for_user})) end end @@ -325,4 +339,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do conn end + + defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do + {:ok, new_user} = User.ensure_keys_present(user) + + for_user = + if new_user != user and match?(%User{}, for_user) do + User.get_cached_by_nickname(for_user.nickname) + else + for_user + end + + {new_user, for_user} + end end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index a05e03263..18145e45f 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -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 it 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"] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d14490bb5..602ae48e1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 4288ea4c8..c146f59d4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -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"])) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 327e0e05b..d9c1bcb2c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -98,29 +98,31 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user, page: page}) do + def render("following.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 end - collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total) + collection(following, "#{user.ap_id}/following", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user}) do + def render("following.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 @@ -130,34 +132,43 @@ defmodule Pleroma.Web.ActivityPub.UserView do "id" => "#{user.ap_id}/following", "type" => "OrderedCollection", "totalItems" => total, - "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + "first" => + if showing do + collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + else + "#{user.ap_id}/following?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user, page: page}) do + def render("followers.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 end - collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total) + collection(followers, "#{user.ap_id}/followers", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user}) do + def render("followers.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 @@ -168,7 +179,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do "type" => "OrderedCollection", "totalItems" => total, "first" => - collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total) + if showing do + collection(followers, "#{user.ap_id}/followers", 1, showing, total) + else + "#{user.ap_id}/followers?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 9908a2e75..2666edc7c 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -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" diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f1450b113..44669b228 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -31,7 +31,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 +176,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 +241,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 - ) + preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false + direct? = visibility == "direct" - res + %{ + 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")} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 8e482eef7..f28a96320 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -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} = diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index c82b20123..46944dcbc 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -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)) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 8c2033c3a..b3513b5bf 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -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,24 @@ 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_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, :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]) @local_mastodon_name "Mastodon-Local" @@ -1051,9 +1068,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}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 62c516f8e..65bab4062 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by: User.following?(target, user), blocking: User.blocks?(user, target), 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, diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index dd8888a02..a661e9bb7 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -3,68 +3,71 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MediaProxy do + alias Pleroma.Config + alias Pleroma.Web + @base64_opts [padding: false] - def url(nil), do: nil - - def url(""), do: nil - + 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 diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex similarity index 83% rename from lib/pleroma/web/media_proxy/controller.ex rename to lib/pleroma/web/media_proxy/media_proxy_controller.ex index a711b54e9..8403850ff 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -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,16 +27,18 @@ 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 && does_not_match(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 diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 4033ec38f..e7fa7f408 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider def build_tags(%{ @@ -81,26 +82,19 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} | acc ] "image" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} | acc @@ -108,8 +102,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do "video" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} | acc ] diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 8dd01e0d5..d6a6049b3 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -1,4 +1,5 @@ # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only @@ -9,13 +10,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider - def build_tags(%{ - activity_id: id, - object: object, - user: user - }) do + def build_tags(%{activity_id: id, object: object, user: user}) do attachments = build_attachments(id, object) scrubbed_content = Utils.scrub_html_and_truncate(object) # Zero width space @@ -27,21 +25,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, - [ - property: "twitter:description", - content: content - ], []} + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ - {:meta, - [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary_large_image"], []} ] else @@ -53,30 +42,28 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do def build_tags(%{user: user}) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, + title_tag(user), {:meta, [property: "twitter:description", content: truncated_bio], []}, - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary"], []} ] end end + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ {:meta, [property: "twitter:card", content: "player"], []}, diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 58385a3d1..720bd4519 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -39,4 +39,11 @@ defmodule Pleroma.Web.Metadata.Utils do "(@#{user.nickname})" end end + + @spec fetch_media_type(list(String.t()), String.t()) :: String.t() | nil + def fetch_media_type(supported_types, media_type) do + Enum.find(supported_types, fn support_type -> + String.starts_with?(media_type, support_type) + end) + end end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index cd9a4f4a8..a1d7fcc7d 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -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 %{} diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 21cd47890..0d2523338 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -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 @@ -48,7 +46,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/router.ex b/lib/pleroma/web/router.ex index d53fa8a35..3e5142e8a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -322,10 +322,6 @@ defmodule Pleroma.Web.Router do patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) - patch("/accounts/update_avatar", MastodonAPIController, :update_avatar) - patch("/accounts/update_banner", MastodonAPIController, :update_banner) - patch("/accounts/update_background", MastodonAPIController, :update_background) - post("/statuses", MastodonAPIController, :post_status) delete("/statuses/:id", MastodonAPIController, :delete_status) @@ -360,6 +356,10 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) + patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) + patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) + patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background) + get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) @@ -623,8 +623,6 @@ defmodule Pleroma.Web.Router do # XXX: not really ostatus pipe_through(:ostatus) - get("/users/:nickname/followers", ActivityPubController, :followers) - get("/users/:nickname/following", ActivityPubController, :following) get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/objects/:uuid/likes", ActivityPubController, :object_likes) end @@ -656,6 +654,12 @@ defmodule Pleroma.Web.Router do pipe_through(:oauth_write) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) end + + scope [] do + pipe_through(:oauth_read_or_public) + get("/users/:nickname/followers", ActivityPubController, :followers) + get("/users/:nickname/following", ActivityPubController, :following) + end end scope "/relay", Pleroma.Web.ActivityPub do diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index e96e4e1e4..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -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", []) - to - |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) + 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, 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) diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 45ef7be3d..0313560a8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -192,6 +192,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 diff --git a/mix.exs b/mix.exs index f96789d21..a26bb6202 100644 --- 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"}, diff --git a/mix.lock b/mix.lock index 9c0fd0e98..dcbf80f01 100644 --- 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"}, @@ -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 index 000000000..3c32bc355 --- /dev/null +++ b/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs @@ -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 index 000000000..50669902e --- /dev/null +++ b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs @@ -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 diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index f36b231c5..57ed05eba 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -20,6 +20,10 @@ "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/list_test.exs b/test/list_test.exs index 1909c0cd9..f39033d02 100644 --- a/test/list_test.exs +++ b/test/list_test.exs @@ -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 diff --git a/test/notification_test.exs b/test/notification_test.exs index 1d36f14bf..dda570b49 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -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 diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs index 1beed6236..61cd1b412 100644 --- a/test/object/containment_test.exs +++ b/test/object/containment_test.exs @@ -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 diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 3b666e0d1..56a9d775f 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -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 diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs index f8251b5c7..395095079 100644 --- a/test/plugs/rate_limiter_test.exs +++ b/test/plugs/rate_limiter_test.exs @@ -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) diff --git a/test/signature_test.exs b/test/signature_test.exs new file mode 100644 index 000000000..4920196c7 --- /dev/null +++ b/test/signature_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SignatureTest do + use Pleroma.DataCase + + 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 + } + + 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(%Plug.Conn{params: %{"actor" => user.ap_id}}) == + expected_result + end + + test "it returns error when not found user" do + assert Signature.fetch_public_key(%Plug.Conn{params: %{"actor" => "test-ap_id"}}) == + {:error, :error} + end + + test "it returns error if public key is empty" do + user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}}) + + assert Signature.fetch_public_key(%Plug.Conn{params: %{"actor" => 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(%Plug.Conn{params: %{"actor" => ap_id}}) == + {:ok, @rsa_public_key} + end + + test "it returns error when not found user" do + assert Signature.refetch_public_key(%Plug.Conn{params: %{"actor" => "test-ap_id"}}) == + {:error, {:error, :ok}} + 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 diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ff6bb78f9..7811f7807 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -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)}, #{ diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index bbcc57217..a9b79eb5b 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -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/user_test.exs b/test/user_test.exs index 7c3fe976d..264b7a40e 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -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 diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 1f8eb9d71..452172bb4 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -551,7 +552,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["first"]["orderedItems"] == [user.ap_id] end - test "it returns returns empty if the user has 'hide_followers' set", %{conn: conn} do + test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do user = insert(:user) user_two = insert(:user, %{info: %{hide_followers: true}}) User.follow(user, user_two) @@ -561,8 +562,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> get("/users/#{user_two.nickname}/followers") |> json_response(200) - assert result["first"]["orderedItems"] == [] - assert result["totalItems"] == 0 + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated", + %{conn: conn} do + user = insert(:user, %{info: %{hide_followers: true}}) + + result = + conn + |> get("/users/#{user.nickname}/followers?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, %{info: %{hide_followers: true}}) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] end test "it works for more than 10 users", %{conn: conn} do @@ -606,7 +634,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["first"]["orderedItems"] == [user_two.ap_id] end - test "it returns returns empty if the user has 'hide_follows' set", %{conn: conn} do + test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do user = insert(:user, %{info: %{hide_follows: true}}) user_two = insert(:user) User.follow(user, user_two) @@ -616,8 +644,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> get("/users/#{user.nickname}/following") |> json_response(200) - assert result["first"]["orderedItems"] == [] - assert result["totalItems"] == 0 + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated", + %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + + result = + conn + |> get("/users/#{user.nickname}/following?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] end test "it works for more than 10 users", %{conn: conn} do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 59d56f3a7..00adbc0f9 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -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/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index b896a532b..a1f5f6e36 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -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 diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 969860c4c..86254117f 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.CommonAPI test "Renders a user, including the public key" do user = insert(:user) @@ -82,4 +83,28 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do refute result["endpoints"]["oauthTokenEndpoint"] end end + + describe "followers" do + test "sets totalItems to zero when followers are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + info = Map.put(user.info, :hide_followers, true) + user = Map.put(user, :info, info) + assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user}) + end + end + + describe "following" do + test "sets totalItems to zero when follows are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + info = Map.put(user.info, :hide_follows, true) + user = Map.put(user, :info, info) + assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) + end + end end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index 4d5c07da4..b62a89e68 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -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) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 1b71cbff3..ee48b752c 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -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 diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 958c931c4..7d8eeb12e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -129,6 +129,18 @@ 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 end describe "reactions" do @@ -346,6 +358,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}) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 8afb1497b..6ffa64dc8 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -593,7 +593,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_avatar", %{img: avatar_image}) + |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image}) user = refresh_record(user) @@ -618,7 +618,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_avatar", %{img: ""}) + |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""}) user = User.get_cached_by_id(user.id) @@ -633,7 +633,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_banner", %{"banner" => @image}) + |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image}) user = refresh_record(user) assert user.info.banner["type"] == "Image" @@ -647,7 +647,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_banner", %{"banner" => ""}) + |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""}) user = refresh_record(user) assert user.info.banner == %{} @@ -661,7 +661,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_background", %{"img" => @image}) + |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image}) user = refresh_record(user) assert user.info.background["type"] == "Image" @@ -674,7 +674,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn = conn |> assign(:user, user) - |> patch("/api/v1/accounts/update_background", %{"img" => ""}) + |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""}) user = refresh_record(user) assert user.info.background == %{} @@ -1274,6 +1274,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 @@ -2105,25 +2170,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") - assert %{"id" => _id, "muting" => false} = json_response(conn, 200) + response = json_response(conn, 200) + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response + end + + test "without notifications", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) + + response = json_response(conn, 200) + + assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response + user = User.get_cached_by_id(user.id) + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/unmute") + + response = json_response(conn, 200) + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response + end end test "subscribing / unsubscribing to a user", %{conn: conn} do diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index ac42819d8..995bd52c8 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -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 index 000000000..53b8f556b --- /dev/null +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# 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 = "You are being redirected." + 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 diff --git a/test/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs similarity index 93% rename from test/media_proxy_test.exs rename to test/web/media_proxy/media_proxy_test.exs index 176b09914..edbbf9b66 100644 --- a/test/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2018 Pleroma Authors # 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,28 @@ 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 diff --git a/test/web/metadata/player_view_test.exs b/test/web/metadata/player_view_test.exs new file mode 100644 index 000000000..742b0ed8b --- /dev/null +++ b/test/web/metadata/player_view_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.PlayerViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.Metadata.PlayerView + + test "it renders audio tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "audio", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "" + end + + test "it renders videos tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "video", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "" + end +end diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs new file mode 100644 index 000000000..0814006d2 --- /dev/null +++ b/test/web/metadata/twitter_card_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.TwitterCard + alias Pleroma.Web.Metadata.Utils + alias Pleroma.Web.Router + + test "it renders twitter card for user info" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + avatar_url = Utils.attachment_url(User.avatar_url(user)) + res = TwitterCard.build_tags(%{user: user}) + + assert res == [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, + {:meta, [property: "twitter:image", content: avatar_url], []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end + + test "it does not render attachments if post is nsfw" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "sensitive" => true, + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + []}, + {:meta, [property: "twitter:card", content: "summary_large_image"], []} + ] == result + end + + test "it renders supported types of attachments and skips unknown types" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, + [ + property: "twitter:player", + content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) + ], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + ] == result + end +end diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index be1173513..d7f848bfa 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -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 diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 7ec0e101d..de6177575 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -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