# Conflicts: # lib/pleroma/web/media_proxy/media_proxy_controller.extags/v1.1.4
@@ -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. | |||
@@ -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. <https://github.com/tootsuite/mastodon/pull/11196> | |||
- 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. <https://github.com/tootsuite/mastodon/pull/11196> | |||
- 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 | |||
@@ -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. | |||
@@ -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` | |||
@@ -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` | |||
@@ -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 |
@@ -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 | |||
@@ -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) | |||
@@ -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) | |||
@@ -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 """ | |||
@@ -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 |
@@ -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 |
@@ -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, | |||
@@ -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 |
@@ -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 | |||
@@ -31,12 +31,28 @@ defmodule Pleroma.Plugs.RateLimiter do | |||
## Usage | |||
AllowedSyntax: | |||
plug(Pleroma.Plugs.RateLimiter, :limiter_name) | |||
plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options}) | |||
Allowed options: | |||
* `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions) | |||
* `params` appends values of specified request params (e.g. ["id"]) to bucket name | |||
Inside a controller: | |||
plug(Pleroma.Plugs.RateLimiter, :one when action == :one) | |||
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) | |||
or inside a router pipiline: | |||
plug( | |||
Pleroma.Plugs.RateLimiter, | |||
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} | |||
when action in ~w(fav_status unfav_status)a | |||
) | |||
or inside a router pipeline: | |||
pipeline :api do | |||
... | |||
@@ -49,33 +65,56 @@ defmodule Pleroma.Plugs.RateLimiter do | |||
alias Pleroma.User | |||
def init(limiter_name) do | |||
def init(limiter_name) when is_atom(limiter_name) do | |||
init({limiter_name, []}) | |||
end | |||
def init({limiter_name, opts}) do | |||
case Pleroma.Config.get([:rate_limit, limiter_name]) do | |||
nil -> nil | |||
config -> {limiter_name, config} | |||
config -> {limiter_name, config, opts} | |||
end | |||
end | |||
# do not limit if there is no limiter configuration | |||
# Do not limit if there is no limiter configuration | |||
def call(conn, nil), do: conn | |||
def call(conn, opts) do | |||
case check_rate(conn, opts) do | |||
{:ok, _count} -> conn | |||
{:error, _count} -> render_throttled_error(conn) | |||
def call(conn, settings) do | |||
case check_rate(conn, settings) do | |||
{:ok, _count} -> | |||
conn | |||
{:error, _count} -> | |||
render_throttled_error(conn) | |||
end | |||
end | |||
defp bucket_name(conn, limiter_name, opts) do | |||
bucket_name = opts[:bucket_name] || limiter_name | |||
if params_names = opts[:params] do | |||
params_values = for p <- Enum.sort(params_names), do: conn.params[p] | |||
Enum.join([bucket_name] ++ params_values, ":") | |||
else | |||
bucket_name | |||
end | |||
end | |||
defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do | |||
ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit) | |||
defp check_rate( | |||
%{assigns: %{user: %User{id: user_id}}} = conn, | |||
{limiter_name, [_, {scale, limit}], opts} | |||
) do | |||
bucket_name = bucket_name(conn, limiter_name, opts) | |||
ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit) | |||
end | |||
defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do | |||
ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit) | |||
defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do | |||
bucket_name = bucket_name(conn, limiter_name, opts) | |||
ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit) | |||
end | |||
defp check_rate(conn, {limiter_name, {scale, limit}}) do | |||
check_rate(conn, {limiter_name, [{scale, limit}]}) | |||
defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do | |||
check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) | |||
end | |||
def ip(%{remote_ip: remote_ip}) do | |||
@@ -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() | |||
@@ -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 | |||
@@ -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 | |||
@@ -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: | |||
@@ -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 |
@@ -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"] | |||
@@ -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 | |||
@@ -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"])) | |||
@@ -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 | |||
@@ -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" | |||
@@ -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 | |||
) | |||
res | |||
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false | |||
direct? = visibility == "direct" | |||
%{ | |||
to: to, | |||
actor: user, | |||
context: context, | |||
object: object, | |||
additional: %{"cc" => cc, "directMessage" => direct?} | |||
} | |||
|> maybe_add_list_data(user, visibility) | |||
|> ActivityPub.create(preview?) | |||
else | |||
{:private_to_public, true} -> | |||
{:error, dgettext("errors", "The message visibility must be direct")} | |||
@@ -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} = | |||
@@ -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)) | |||
@@ -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}) | |||
@@ -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, | |||
@@ -3,68 +3,71 @@ | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.MediaProxy do | |||
@base64_opts [padding: false] | |||
def url(nil), do: nil | |||
alias Pleroma.Config | |||
alias Pleroma.Web | |||
def url(""), do: nil | |||
@base64_opts [padding: false] | |||
def url(url) when is_nil(url) or url == "", do: nil | |||
def url("/" <> _ = url), do: url | |||
def url(url) do | |||
if !enabled?() or local?(url) or whitelisted?(url) do | |||
if disabled?() or local?(url) or whitelisted?(url) do | |||
url | |||
else | |||
encode_url(url) | |||
end | |||
end | |||
defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false) | |||
defp disabled?, do: !Config.get([:media_proxy, :enabled], false) | |||
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) | |||
defp whitelisted?(url) do | |||
%{host: domain} = URI.parse(url) | |||
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> | |||
Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern -> | |||
String.equivalent?(domain, pattern) | |||
end) | |||
end | |||
def encode_url(url) do | |||
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) | |||
base64 = Base.url_encode64(url, @base64_opts) | |||
sig = :crypto.hmac(:sha, secret, base64) | |||
sig64 = sig |> Base.url_encode64(@base64_opts) | |||
sig64 = | |||
base64 | |||
|> signed_url | |||
|> Base.url_encode64(@base64_opts) | |||
build_url(sig64, base64, filename(url)) | |||
end | |||
def decode_url(sig, url) do | |||
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) | |||
sig = Base.url_decode64!(sig, @base64_opts) | |||
local_sig = :crypto.hmac(:sha, secret, url) | |||
if local_sig == sig do | |||
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), | |||
signature when signature == sig <- signed_url(url) do | |||
{:ok, Base.url_decode64!(url, @base64_opts)} | |||
else | |||
{:error, :invalid_signature} | |||
_ -> {:error, :invalid_signature} | |||
end | |||
end | |||
defp signed_url(url) do | |||
:crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) | |||
end | |||
def filename(url_or_path) do | |||
if path = URI.parse(url_or_path).path, do: Path.basename(path) | |||
end | |||
def build_url(sig_base64, url_base64, filename \\ nil) do | |||
[ | |||
Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()), | |||
Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), | |||
"proxy", | |||
sig_base64, | |||
url_base64, | |||
filename | |||
] | |||
|> Enum.filter(fn value -> value end) | |||
|> Enum.filter(& &1) | |||
|> Path.join() | |||
end | |||
end |
@@ -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 |
@@ -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 | |||
] | |||
@@ -1,4 +1,5 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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"], []}, | |||
@@ -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 |
@@ -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 | |||
%{} | |||
@@ -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} | |||
@@ -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 | |||
@@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do | |||
{:ok, salmon} | |||
end | |||
def remote_users(%{data: %{"to" => to} = data}) do | |||
to = to ++ (data["cc"] || []) | |||
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do | |||
cc = Map.get(data, "cc", []) | |||
bcc = | |||
data | |||
|> Map.get("bcc", []) | |||
|> Enum.reduce([], fn ap_id, bcc -> | |||
case Pleroma.List.get_by_ap_id(ap_id) do | |||
%Pleroma.List{user_id: ^user_id} = list -> | |||
{:ok, following} = Pleroma.List.get_following(list) | |||
bcc ++ Enum.map(following, & &1.ap_id) | |||
_ -> | |||
bcc | |||
end | |||
end) | |||
to | |||
|> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) | |||
[to, cc, bcc] | |||
|> Enum.concat() | |||
|> Enum.map(&User.get_cached_by_ap_id/1) | |||
|> Enum.filter(fn user -> user && !user.local end) | |||
end | |||
@@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do | |||
{:ok, private, _} = Keys.keys_from_pem(keys) | |||
{:ok, feed} = encode(private, feed) | |||
remote_users = remote_users(activity) | |||
remote_users = remote_users(user, activity) | |||
salmon_urls = Enum.map(remote_users, & &1.info.salmon) | |||
reachable_urls_metadata = Instances.filter_reachable(salmon_urls) | |||
@@ -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 | |||
@@ -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"}, | |||
@@ -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"}, | |||
@@ -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 |
@@ -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 |
@@ -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" | |||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | |||
@@ -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) | |||
@@ -0,0 +1,99 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.SignatureTest do | |||
use Pleroma.DataCase | |||
import 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 |
@@ -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)}, #{ | |||
@@ -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 |
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 |
@@ -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) | |||
@@ -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 | |||
@@ -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}) | |||
@@ -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 | |||
@@ -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 |
@@ -0,0 +1,73 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do | |||
use Pleroma.Web.ConnCase | |||
import Mock | |||
alias Pleroma.Config | |||
setup do | |||
media_proxy_config = Config.get([:media_proxy]) || [] | |||
on_exit(fn -> Config.put([:media_proxy], media_proxy_config) end) | |||
:ok | |||
end | |||
test "it returns 404 when MediaProxy disabled", %{conn: conn} do | |||
Config.put([:media_proxy, :enabled], false) | |||
assert %Plug.Conn{ | |||
status: 404, | |||
resp_body: "Not Found" | |||
} = get(conn, "/proxy/hhgfh/eeeee") | |||
assert %Plug.Conn{ | |||
status: 404, | |||
resp_body: "Not Found" | |||
} = get(conn, "/proxy/hhgfh/eeee/fff") | |||
end | |||
test "it returns 403 when signature invalidated", %{conn: conn} do | |||
Config.put([:media_proxy, :enabled], true) | |||
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") | |||
path = URI.parse(Pleroma.Web.MediaProxy.encode_url("https://google.fn")).path | |||
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") | |||
assert %Plug.Conn{ | |||
status: 403, | |||
resp_body: "Forbidden" | |||
} = get(conn, path) | |||
assert %Plug.Conn{ | |||
status: 403, | |||
resp_body: "Forbidden" | |||
} = get(conn, "/proxy/hhgfh/eeee") | |||
assert %Plug.Conn{ | |||
status: 403, | |||
resp_body: "Forbidden" | |||
} = get(conn, "/proxy/hhgfh/eeee/fff") | |||
end | |||
test "redirects on valid url when filename invalidated", %{conn: conn} do | |||
Config.put([:media_proxy, :enabled], true) | |||
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") | |||
url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") | |||
invalid_url = String.replace(url, "test.png", "test-file.png") | |||
response = get(conn, invalid_url) | |||
html = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>" | |||
assert response.status == 302 | |||
assert response.resp_body == html | |||
end | |||
test "it performs ReverseProxy.call when signature valid", %{conn: conn} do | |||
Config.put([:media_proxy, :enabled], true) | |||
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") | |||
url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") | |||
with_mock Pleroma.ReverseProxy, | |||
call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do | |||
assert %Plug.Conn{status: :success} = get(conn, url) | |||
end | |||
end | |||
end |
@@ -2,7 +2,7 @@ | |||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.MediaProxyTest do | |||
defmodule Pleroma.Web.MediaProxyTest do | |||
use ExUnit.Case | |||
import Pleroma.Web.MediaProxy | |||
alias Pleroma.Web.MediaProxy.MediaProxyController | |||
@@ -90,22 +90,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 |
@@ -0,0 +1,33 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.Metadata.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 == | |||
"<audio controls><source src=\"test-href\" type=\"audio\">Your browser does not support audio playback.</audio>" | |||
end | |||
test "it renders videos tag" do | |||
res = | |||
PlayerView.render( | |||
"player.html", | |||
%{"mediaType" => "video", "href" => "test-href"} | |||
) | |||
|> Phoenix.HTML.safe_to_string() | |||
assert res == | |||
"<video controls loop><source src=\"test-href\" type=\"video\">Your browser does not support video playback.</video>" | |||
end | |||
end |
@@ -0,0 +1,123 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.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 |
@@ -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 |
@@ -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 | |||