@@ -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. | |||
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text | |||
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set | |||
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option | |||
- Mastodon API: Unsubscribe followers when they unfollow a user | |||
### Fixed | |||
- Not being able to pin unlisted posts | |||
@@ -23,23 +24,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
### Added | |||
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) | |||
- MRF: Support for excluding specific domains from Transparency. | |||
- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`) | |||
- Configuration: `federation_incoming_replies_max_depth` option | |||
- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses) | |||
- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header | |||
- Mastodon API, extension: Ability to reset avatar, profile banner, and background | |||
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196> | |||
- Mastodon API: Add support for muting/unmuting notifications | |||
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373> | |||
- Mastodon API: Add `pleroma.deactivated` to the Account entity | |||
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit. | |||
- Admin API: Return users' tags when querying reports | |||
- Admin API: Return avatar and display name when querying users | |||
- Admin API: Allow querying user by ID | |||
- Admin API: Added support for `tuples`. | |||
- Added synchronization of following/followers counters for external users | |||
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. | |||
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196> | |||
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. | |||
- Addressable lists | |||
- Twitter API: added rate limit for `/api/account/password_reset` endpoint. | |||
- ActivityPub: Add an internal service actor for fetching ActivityPub objects. | |||
- ActivityPub: Optional signing of ActivityPub object fetches. | |||
- Admin API: Endpoint for fetching latest user's statuses | |||
### 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 | |||
@@ -305,7 +305,8 @@ config :pleroma, :activitypub, | |||
accept_blocks: true, | |||
unfollow_blocked: true, | |||
outgoing_blocks: true, | |||
follow_handshake_timeout: 500 | |||
follow_handshake_timeout: 500, | |||
sign_object_fetches: true | |||
config :pleroma, :user, deny_follow_blocked: true | |||
@@ -339,7 +340,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, | |||
@@ -523,8 +529,11 @@ config :http_signatures, | |||
config :pleroma, :rate_limit, | |||
search: [{1000, 10}, {1000, 30}], | |||
app_account_creation: {1_800_000, 25}, | |||
relations_actions: {10_000, 10}, | |||
relation_id_action: {60_000, 2}, | |||
statuses_actions: {10_000, 15}, | |||
status_id_action: {60_000, 3} | |||
status_id_action: {60_000, 3}, | |||
password_reset: {1_800_000, 5} | |||
# Import environment specific config. This must remain at the bottom | |||
# of this file so it overrides the configuration defined above. | |||
@@ -31,6 +31,8 @@ config :pleroma, :instance, | |||
skip_thread_containment: false, | |||
federating: false | |||
config :pleroma, :activitypub, sign_object_fetches: false | |||
# Configure your database | |||
config :pleroma, Pleroma.Repo, | |||
adapter: Ecto.Adapters.Postgres, | |||
@@ -67,7 +69,8 @@ config :pleroma, Pleroma.ScheduledActivity, | |||
config :pleroma, :rate_limit, | |||
search: [{1000, 30}, {1000, 30}], | |||
app_account_creation: {10_000, 5} | |||
app_account_creation: {10_000, 5}, | |||
password_reset: {1000, 30} | |||
config :pleroma, :http_security, report_uri: "https://endpoint.com" | |||
@@ -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` | |||
@@ -45,6 +47,7 @@ Has these additional fields under the `pleroma` object: | |||
- `hide_follows`: boolean, true when the user has follow hiding enabled | |||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` | |||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` | |||
- `deactivated`: boolean, true when the user is deactivated | |||
### Source | |||
@@ -72,6 +75,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` | |||
@@ -31,6 +31,7 @@ Feel free to contact us to be added to this list! | |||
- Features: No Streaming | |||
### Fedilab | |||
- Homepage: <https://fedilab.app/> | |||
- Source Code: <https://gitlab.com/tom79/mastalab/> | |||
- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79) | |||
- Platforms: Android | |||
@@ -101,6 +101,7 @@ config :pleroma, Pleroma.Emails.Mailer, | |||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. | |||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. | |||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. | |||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (see `:mrf_mention` section) | |||
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. | |||
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. | |||
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json`` | |||
@@ -271,6 +272,9 @@ config :pleroma, :mrf_subchain, | |||
* `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html) | |||
* `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html) | |||
## :mrf_mention | |||
* `actors`: A list of actors, for which to drop any posts mentioning. | |||
## :media_proxy | |||
* `enabled`: Enables proxying of remote media to the instance’s proxy | |||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. | |||
@@ -328,6 +332,7 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start | |||
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed | |||
* ``outgoing_blocks``: Whether to federate blocks to other instances | |||
* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question | |||
* ``sign_object_fetches``: Sign object fetches with HTTP signatures | |||
## :http_security | |||
* ``enabled``: Whether the managed content security policy is enabled | |||
@@ -425,6 +430,7 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`. | |||
* `enabled`: if enabled the instance will parse metadata from attached links to generate link previews | |||
* `ignore_hosts`: list of hosts which will be ignored by the metadata parser. For example `["accounts.google.com", "xss.website"]`, defaults to `[]`. | |||
* `ignore_tld`: list TLDs (top-level domains) which will ignore for parse metadata. default is ["local", "localdomain", "lan"] | |||
* `parsers`: list of Rich Media parsers | |||
## :fetch_initial_posts | |||
* `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts | |||
@@ -646,5 +652,7 @@ Supported rate limiters: | |||
* `:search` for the search requests (account & status search etc.) | |||
* `:app_account_creation` for registering user accounts from the same IP address | |||
* `:relations_actions` for actions on relations with all users (follow, unfollow) | |||
* `:relation_id_action` for actions on relation with a specific user (follow, unfollow) | |||
* `:statuses_actions` for create / delete / fav / unfav / reblog / unreblog actions on any statuses | |||
* `:status_id_action` for fav / unfav or reblog / unreblog actions on the same status by the same user |
@@ -24,7 +24,9 @@ If you came here from one of the installation guides, take a look at the example | |||
``` | |||
config :pleroma, :media_proxy, | |||
enabled: true, | |||
redirect_on_failure: true | |||
proxy_opts: [ | |||
redirect_on_failure: true | |||
] | |||
#base_url: "https://cache.pleroma.social" | |||
``` | |||
If you want to use a subdomain to serve the files, uncomment `base_url`, change the url and add a comma after `true` in the previous line. | |||
@@ -202,7 +202,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress | |||
#### Further reading | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -200,7 +200,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress | |||
#### Further reading | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -264,7 +264,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress | |||
#### Further reading | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -190,7 +190,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress | |||
#### Further reading | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -180,7 +180,6 @@ mix set_moderator username [true|false] | |||
#### コンフィギュレーションとカスタマイズ | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -283,7 +283,6 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a | |||
#### Further reading | |||
* [Admin tasks](Admin tasks) | |||
* [Backup your instance](backup.html) | |||
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) | |||
* [Hardening your instance](hardening.html) | |||
@@ -242,6 +242,14 @@ So for example, if the task is `mix pleroma.user set admin --admin`, you should | |||
```sh | |||
su pleroma -s $SHELL -lc "./bin/pleroma_ctl user set admin --admin" | |||
``` | |||
## Create your first user and set as admin | |||
```sh | |||
cd /opt/pleroma/bin | |||
su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin" | |||
``` | |||
This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password. | |||
### Updating | |||
Generally, doing the following is enough: | |||
```sh | |||
@@ -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 | |||
@@ -140,6 +140,11 @@ defmodule Pleroma.Application do | |||
id: :federator_init, | |||
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]}, | |||
restart: :temporary | |||
}, | |||
%{ | |||
id: :internal_fetch_init, | |||
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]}, | |||
restart: :temporary | |||
} | |||
] ++ | |||
streamer_child() ++ | |||
@@ -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) | |||
@@ -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.user_id)) | |||
end | |||
end | |||
def for_user(user, opts \\ %{}) do | |||
user | |||
|> for_user_query() | |||
|> for_user_query(opts) | |||
|> Pagination.fetch_paginated(opts) | |||
end | |||
@@ -179,11 +194,10 @@ defmodule Pleroma.Notification do | |||
def get_notified_from_activity(_, _local_only), do: [] | |||
@spec skip?(Activity.t(), User.t()) :: boolean() | |||
def skip?(activity, user) do | |||
[ | |||
:self, | |||
:blocked, | |||
:muted, | |||
:followers, | |||
:follows, | |||
:non_followers, | |||
@@ -193,21 +207,11 @@ defmodule Pleroma.Notification do | |||
|> Enum.any?(&skip?(&1, activity, user)) | |||
end | |||
@spec skip?(atom(), Activity.t(), User.t()) :: boolean() | |||
def skip?(:self, activity, user) do | |||
activity.data["actor"] == user.ap_id | |||
end | |||
def skip?(:blocked, activity, user) do | |||
actor = activity.data["actor"] | |||
User.blocks?(user, %{ap_id: actor}) | |||
end | |||
def skip?(:muted, activity, user) do | |||
actor = activity.data["actor"] | |||
User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity) | |||
end | |||
def skip?( | |||
:followers, | |||
activity, | |||
@@ -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 |
@@ -6,6 +6,8 @@ defmodule Pleroma.Object.Fetcher do | |||
alias Pleroma.HTTP | |||
alias Pleroma.Object | |||
alias Pleroma.Object.Containment | |||
alias Pleroma.Signature | |||
alias Pleroma.Web.ActivityPub.InternalFetchActor | |||
alias Pleroma.Web.ActivityPub.Transmogrifier | |||
alias Pleroma.Web.OStatus | |||
@@ -32,33 +34,39 @@ defmodule Pleroma.Object.Fetcher do | |||
else | |||
Logger.info("Fetching #{id} via AP") | |||
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), | |||
nil <- Object.normalize(data, false), | |||
with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, | |||
{:normalize, nil} <- {:normalize, Object.normalize(data, false)}, | |||
params <- %{ | |||
"type" => "Create", | |||
"to" => data["to"], | |||
"cc" => data["cc"], | |||
# Should we seriously keep this attributedTo thing? | |||
"actor" => data["actor"] || data["attributedTo"], | |||
"object" => data | |||
}, | |||
:ok <- Containment.contain_origin(id, params), | |||
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)}, | |||
{:ok, activity} <- Transmogrifier.handle_incoming(params, options), | |||
{:object, _data, %Object{} = object} <- | |||
{:object, data, Object.normalize(activity, false)} do | |||
{:ok, object} | |||
else | |||
{:containment, _} -> | |||
{:error, "Object containment failed."} | |||
{:error, {:reject, nil}} -> | |||
{:reject, nil} | |||
{:object, data, nil} -> | |||
reinject_object(data) | |||
object = %Object{} -> | |||
{:normalize, object = %Object{}} -> | |||
{:ok, object} | |||
_e -> | |||
# Only fallback when receiving a fetch/normalization error with ActivityPub | |||
Logger.info("Couldn't get object via AP, trying out OStatus fetching...") | |||
# FIXME: OStatus Object Containment? | |||
case OStatus.fetch_activity_from_url(id) do | |||
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)} | |||
e -> e | |||
@@ -76,15 +84,52 @@ defmodule Pleroma.Object.Fetcher do | |||
end | |||
end | |||
defp make_signature(id, date) do | |||
uri = URI.parse(id) | |||
signature = | |||
InternalFetchActor.get_actor() | |||
|> Signature.sign(%{ | |||
"(request-target)": "get #{uri.path}", | |||
host: uri.host, | |||
date: date | |||
}) | |||
[{:Signature, signature}] | |||
end | |||
defp sign_fetch(headers, id, date) do | |||
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do | |||
headers ++ make_signature(id, date) | |||
else | |||
headers | |||
end | |||
end | |||
defp maybe_date_fetch(headers, date) do | |||
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do | |||
headers ++ [{:Date, date}] | |||
else | |||
headers | |||
end | |||
end | |||
def fetch_and_contain_remote_object_from_id(id) do | |||
Logger.info("Fetching object #{id} via AP") | |||
date = | |||
NaiveDateTime.utc_now() | |||
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") | |||
headers = | |||
[{:Accept, "application/activity+json"}] | |||
|> maybe_date_fetch(date) | |||
|> sign_fetch(id, date) | |||
Logger.debug("Fetch headers: #{inspect(headers)}") | |||
with true <- String.starts_with?(id, "http"), | |||
{:ok, %{body: body, status: code}} when code in 200..299 <- | |||
HTTP.get( | |||
id, | |||
[{:Accept, "application/activity+json"}] | |||
), | |||
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers), | |||
{:ok, data} <- Jason.decode(body), | |||
:ok <- Containment.contain_origin_from_id(id, data) do | |||
{:ok, data} | |||
@@ -6,9 +6,21 @@ defmodule Pleroma.Plugs.AuthenticationPlug do | |||
alias Comeonin.Pbkdf2 | |||
import Plug.Conn | |||
alias Pleroma.User | |||
require Logger | |||
def init(options) do | |||
options | |||
def init(options), do: options | |||
def checkpw(password, "$6" <> _ = password_hash) do | |||
:crypt.crypt(password, password_hash) == password_hash | |||
end | |||
def checkpw(password, "$pbkdf2" <> _ = password_hash) do | |||
Pbkdf2.checkpw(password, password_hash) | |||
end | |||
def checkpw(_password, _password_hash) do | |||
Logger.error("Password hash not recognized") | |||
false | |||
end | |||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn | |||
@@ -8,10 +8,16 @@ defmodule Pleroma.Signature do | |||
alias Pleroma.Keys | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Utils | |||
defp key_id_to_actor_id(key_id) do | |||
URI.parse(key_id) | |||
|> Map.put(:fragment, nil) | |||
|> URI.to_string() | |||
end | |||
def fetch_public_key(conn) do | |||
with actor_id <- Utils.get_ap_id(conn.params["actor"]), | |||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), | |||
actor_id <- key_id_to_actor_id(kid), | |||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do | |||
{:ok, public_key} | |||
else | |||
@@ -21,7 +27,8 @@ defmodule Pleroma.Signature do | |||
end | |||
def refetch_public_key(conn) do | |||
with actor_id <- Utils.get_ap_id(conn.params["actor"]), | |||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), | |||
actor_id <- key_id_to_actor_id(kid), | |||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), | |||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do | |||
{:ok, public_key} | |||
@@ -6,10 +6,19 @@ defmodule Pleroma.Upload.Filter.Dedupe do | |||
@behaviour Pleroma.Upload.Filter | |||
alias Pleroma.Upload | |||
def filter(%Upload{name: name} = upload) do | |||
extension = String.split(name, ".") |> List.last() | |||
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower) | |||
def filter(%Upload{name: name, tempfile: tempfile} = upload) do | |||
extension = | |||
name | |||
|> String.split(".") | |||
|> List.last() | |||
shasum = | |||
:crypto.hash(:sha256, File.read!(tempfile)) | |||
|> Base.encode16(case: :lower) | |||
filename = shasum <> "." <> extension | |||
{:ok, %Upload{upload | id: shasum, path: filename}} | |||
end | |||
def filter(_), do: :ok | |||
end |
@@ -4,6 +4,7 @@ | |||
defmodule Pleroma.Upload.Filter.Mogrifun do | |||
@behaviour Pleroma.Upload.Filter | |||
alias Pleroma.Upload.Filter | |||
@filters [ | |||
{"implode", "1"}, | |||
@@ -34,31 +35,10 @@ defmodule Pleroma.Upload.Filter.Mogrifun do | |||
] | |||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do | |||
filter = Enum.random(@filters) | |||
file | |||
|> Mogrify.open() | |||
|> mogrify_filter(filter) | |||
|> Mogrify.save(in_place: true) | |||
Filter.Mogrify.do_filter(file, [Enum.random(@filters)]) | |||
:ok | |||
end | |||
def filter(_), do: :ok | |||
defp mogrify_filter(mogrify, [filter | rest]) do | |||
mogrify | |||
|> mogrify_filter(filter) | |||
|> mogrify_filter(rest) | |||
end | |||
defp mogrify_filter(mogrify, []), do: mogrify | |||
defp mogrify_filter(mogrify, {action, options}) do | |||
Mogrify.custom(mogrify, action, options) | |||
end | |||
defp mogrify_filter(mogrify, string) when is_binary(string) do | |||
Mogrify.custom(mogrify, string) | |||
end | |||
end |
@@ -11,16 +11,19 @@ defmodule Pleroma.Upload.Filter.Mogrify do | |||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do | |||
filters = Pleroma.Config.get!([__MODULE__, :args]) | |||
do_filter(file, filters) | |||
:ok | |||
end | |||
def filter(_), do: :ok | |||
def do_filter(file, filters) do | |||
file | |||
|> Mogrify.open() | |||
|> mogrify_filter(filters) | |||
|> Mogrify.save(in_place: true) | |||
:ok | |||
end | |||
def filter(_), do: :ok | |||
defp mogrify_filter(mogrify, nil), do: mogrify | |||
defp mogrify_filter(mogrify, [filter | rest]) do | |||
@@ -68,7 +68,14 @@ defmodule Pleroma.Uploaders.Uploader do | |||
{:error, error} | |||
end | |||
after | |||
30_000 -> {:error, dgettext("errors", "Uploader callback timeout")} | |||
callback_timeout() -> {:error, dgettext("errors", "Uploader callback timeout")} | |||
end | |||
end | |||
defp callback_timeout do | |||
case Mix.env() do | |||
:test -> 1_000 | |||
_ -> 30_000 | |||
end | |||
end | |||
end |
@@ -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 | |||
@@ -1146,19 +1157,18 @@ defmodule Pleroma.User do | |||
end | |||
end | |||
def get_or_create_instance_user do | |||
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" | |||
if user = get_cached_by_ap_id(relay_uri) do | |||
@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing." | |||
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do | |||
if user = get_cached_by_ap_id(uri) do | |||
user | |||
else | |||
changes = | |||
%User{info: %User.Info{}} | |||
|> cast(%{}, [:ap_id, :nickname, :local]) | |||
|> put_change(:ap_id, relay_uri) | |||
|> put_change(:nickname, nil) | |||
|> put_change(:ap_id, uri) | |||
|> put_change(:nickname, nickname) | |||
|> put_change(:local, true) | |||
|> put_change(:follower_address, relay_uri <> "/followers") | |||
|> put_change(:follower_address, uri <> "/followers") | |||
{:ok, user} = Repo.insert(changes) | |||
user | |||
@@ -1179,10 +1189,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 +1380,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 | |||
@@ -1405,4 +1410,8 @@ defmodule Pleroma.User do | |||
end | |||
defp put_password_hash(changeset), do: changeset | |||
def is_internal_user?(%User{nickname: nil}), do: true | |||
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true | |||
def is_internal_user?(_), do: false | |||
end |
@@ -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: | |||
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
alias Pleroma.Object.Fetcher | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.InternalFetchActor | |||
alias Pleroma.Web.ActivityPub.ObjectView | |||
alias Pleroma.Web.ActivityPub.Relay | |||
alias Pleroma.Web.ActivityPub.Transmogrifier | |||
@@ -206,9 +207,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
json(conn, dgettext("errors", "error")) | |||
end | |||
def relay(conn, _params) do | |||
with %User{} = user <- Relay.get_actor(), | |||
{:ok, user} <- User.ensure_keys_present(user) do | |||
defp represent_service_actor(%User{} = user, conn) do | |||
with {:ok, user} <- User.ensure_keys_present(user) do | |||
conn | |||
|> put_resp_header("content-type", "application/activity+json") | |||
|> json(UserView.render("user.json", %{user: user})) | |||
@@ -217,6 +217,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
end | |||
end | |||
defp represent_service_actor(nil, _), do: {:error, :not_found} | |||
def relay(conn, _params) do | |||
Relay.get_actor() | |||
|> represent_service_actor(conn) | |||
end | |||
def internal_fetch(conn, _params) do | |||
InternalFetchActor.get_actor() | |||
|> represent_service_actor(conn) | |||
end | |||
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do | |||
conn | |||
|> put_resp_header("content-type", "application/activity+json") | |||
@@ -0,0 +1,20 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ActivityPub.InternalFetchActor do | |||
alias Pleroma.User | |||
require Logger | |||
def init do | |||
# Wait for everything to settle. | |||
Process.sleep(1000 * 5) | |||
get_actor() | |||
end | |||
def get_actor do | |||
"#{Pleroma.Web.Endpoint.url()}/internal/fetch" | |||
|> User.get_or_create_service_actor_by_ap_id("internal.fetch") | |||
end | |||
end |
@@ -0,0 +1,24 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do | |||
@moduledoc "Block messages which mention a user" | |||
@behaviour Pleroma.Web.ActivityPub.MRF | |||
@impl true | |||
def filter(%{"type" => "Create"} = message) do | |||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], []) | |||
recipients = (message["to"] || []) ++ (message["cc"] || []) | |||
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do | |||
{:reject, nil} | |||
else | |||
{:ok, message} | |||
end | |||
end | |||
@impl true | |||
def filter(message), do: {:ok, message} | |||
end |
@@ -92,18 +92,68 @@ defmodule Pleroma.Web.ActivityPub.Publisher do | |||
end | |||
end | |||
@doc """ | |||
Publishes an activity to all relevant peers. | |||
""" | |||
def publish(%User{} = actor, %Activity{} = activity) do | |||
remote_followers = | |||
defp recipients(actor, activity) do | |||
followers = | |||
if actor.follower_address in activity.recipients do | |||
{:ok, followers} = User.get_followers(actor) | |||
followers |> Enum.filter(&(!&1.local)) | |||
Enum.filter(followers, &(!&1.local)) | |||
else | |||
[] | |||
end | |||
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers | |||
end | |||
defp get_cc_ap_ids(ap_id, recipients) do | |||
host = Map.get(URI.parse(ap_id), :host) | |||
recipients | |||
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) | |||
|> Enum.map(& &1.ap_id) | |||
end | |||
@doc """ | |||
Publishes an activity with BCC to all relevant peers. | |||
""" | |||
def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do | |||
public = is_public?(activity) | |||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) | |||
recipients = recipients(actor, activity) | |||
recipients | |||
|> Enum.filter(&User.ap_enabled?/1) | |||
|> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) | |||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end) | |||
|> Instances.filter_reachable() | |||
|> Enum.each(fn {inbox, unreachable_since} -> | |||
%User{ap_id: ap_id} = | |||
Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) | |||
# Get all the recipients on the same host and add them to cc. Otherwise, a remote | |||
# instance would only accept a first message for the first recipient and ignore the rest. | |||
cc = get_cc_ap_ids(ap_id, recipients) | |||
json = | |||
data | |||
|> Map.put("cc", cc) | |||
|> Jason.encode!() | |||
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ | |||
inbox: inbox, | |||
json: json, | |||
actor: actor, | |||
id: activity.data["id"], | |||
unreachable_since: unreachable_since | |||
}) | |||
end) | |||
end | |||
@doc """ | |||
Publishes an activity to all relevant peers. | |||
""" | |||
def publish(%User{} = actor, %Activity{} = activity) do | |||
public = is_public?(activity) | |||
if public && Config.get([:instance, :allow_relay]) do | |||
@@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do | |||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) | |||
json = Jason.encode!(data) | |||
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) | |||
recipients(actor, activity) | |||
|> Enum.filter(fn user -> User.ap_enabled?(user) end) | |||
|> Enum.map(fn %{info: %{source_data: data}} -> | |||
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] | |||
@@ -10,7 +10,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do | |||
require Logger | |||
def get_actor do | |||
User.get_or_create_instance_user() | |||
"#{Pleroma.Web.Endpoint.url()}/relay" | |||
|> User.get_or_create_service_actor_by_ap_id() | |||
end | |||
def follow(target_instance) do | |||
@@ -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"])) | |||
@@ -31,8 +31,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do | |||
def render("endpoints.json", _), do: %{} | |||
# the instance itself is not a Person, but instead an Application | |||
def render("user.json", %{user: %{nickname: nil} = user}) do | |||
def render("service.json", %{user: user}) do | |||
{:ok, user} = User.ensure_keys_present(user) | |||
{:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) | |||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) | |||
@@ -47,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do | |||
"followers" => "#{user.ap_id}/followers", | |||
"inbox" => "#{user.ap_id}/inbox", | |||
"name" => "Pleroma", | |||
"summary" => "Virtual actor for Pleroma relay", | |||
"summary" => | |||
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.", | |||
"url" => user.ap_id, | |||
"manuallyApprovesFollowers" => false, | |||
"publicKey" => %{ | |||
@@ -60,6 +60,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do | |||
|> Map.merge(Utils.make_json_ld_header()) | |||
end | |||
# the instance itself is not a Person, but instead an Application | |||
def render("user.json", %{user: %User{nickname: nil} = user}), | |||
do: render("service.json", %{user: user}) | |||
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), | |||
do: render("service.json", %{user: user}) | |||
def render("user.json", %{user: user}) do | |||
{:ok, user} = User.ensure_keys_present(user) | |||
{:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) | |||
@@ -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" | |||
@@ -3,7 +3,7 @@ | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.Auth.PleromaAuthenticator do | |||
alias Comeonin.Pbkdf2 | |||
alias Pleroma.Plugs.AuthenticationPlug | |||
alias Pleroma.Registration | |||
alias Pleroma.Repo | |||
alias Pleroma.User | |||
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do | |||
def get_user(%Plug.Conn{} = conn) do | |||
with {:ok, {name, password}} <- fetch_credentials(conn), | |||
{_, %User{} = user} <- {:user, fetch_user(name)}, | |||
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do | |||
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do | |||
{:ok, user} | |||
else | |||
error -> | |||
@@ -4,7 +4,6 @@ | |||
defmodule Pleroma.Web.CommonAPI do | |||
alias Pleroma.Activity | |||
alias Pleroma.Bookmark | |||
alias Pleroma.Formatter | |||
alias Pleroma.Object | |||
alias Pleroma.ThreadMute | |||
@@ -31,7 +30,8 @@ defmodule Pleroma.Web.CommonAPI do | |||
def unfollow(follower, unfollowed) do | |||
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), | |||
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do | |||
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), | |||
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do | |||
{:ok, follower} | |||
end | |||
end | |||
@@ -175,6 +175,11 @@ defmodule Pleroma.Web.CommonAPI do | |||
when visibility in ~w{public unlisted private direct}, | |||
do: {visibility, get_replied_to_visibility(in_reply_to)} | |||
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do | |||
visibility = {:list, String.to_integer(list_id)} | |||
{visibility, get_replied_to_visibility(in_reply_to)} | |||
end | |||
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do | |||
visibility = get_replied_to_visibility(in_reply_to) | |||
{visibility, visibility} | |||
@@ -235,19 +240,18 @@ defmodule Pleroma.Web.CommonAPI do | |||
"emoji", | |||
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) | |||
) do | |||
res = | |||
ActivityPub.create( | |||
%{ | |||
to: to, | |||
actor: user, | |||
context: context, | |||
object: object, | |||
additional: %{"cc" => cc, "directMessage" => visibility == "direct"} | |||
}, | |||
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false | |||
) | |||
res | |||
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false | |||
direct? = visibility == "direct" | |||
%{ | |||
to: to, | |||
actor: user, | |||
context: context, | |||
object: object, | |||
additional: %{"cc" => cc, "directMessage" => direct?} | |||
} | |||
|> maybe_add_list_data(user, visibility) | |||
|> ActivityPub.create(preview?) | |||
else | |||
{:private_to_public, true} -> | |||
{:error, dgettext("errors", "The message visibility must be direct")} | |||
@@ -351,15 +355,6 @@ defmodule Pleroma.Web.CommonAPI do | |||
end | |||
end | |||
def bookmarked?(user, activity) do | |||
with %Bookmark{} <- Bookmark.get(user.id, activity.id) do | |||
true | |||
else | |||
_ -> | |||
false | |||
end | |||
end | |||
def report(user, data) do | |||
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, | |||
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, | |||
@@ -6,11 +6,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
import Pleroma.Web.Gettext | |||
alias Calendar.Strftime | |||
alias Comeonin.Pbkdf2 | |||
alias Pleroma.Activity | |||
alias Pleroma.Config | |||
alias Pleroma.Formatter | |||
alias Pleroma.Object | |||
alias Pleroma.Plugs.AuthenticationPlug | |||
alias Pleroma.Repo | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.Utils | |||
@@ -100,12 +100,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
end | |||
end | |||
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} | |||
def get_addressed_users(_, to) when is_list(to) do | |||
User.get_ap_ids_by_nicknames(to) | |||
end | |||
def get_addressed_users(mentioned_users, _), do: mentioned_users | |||
def maybe_add_list_data(activity_params, user, {:list, list_id}) do | |||
case Pleroma.List.get(list_id, user) do | |||
%Pleroma.List{} = list -> | |||
activity_params | |||
|> put_in([:additional, "bcc"], [list.ap_id]) | |||
|> put_in([:additional, "listMessage"], list.ap_id) | |||
|> put_in([:object, "listMessage"], list.ap_id) | |||
_ -> | |||
activity_params | |||
end | |||
end | |||
def maybe_add_list_data(activity_params, _, _), do: activity_params | |||
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) | |||
when is_list(options) do | |||
%{max_expiration: max_expiration, min_expiration: min_expiration} = | |||
@@ -371,7 +388,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
def confirm_current_password(user, password) do | |||
with %User{local: true} = db_user <- User.get_cached_by_id(user.id), | |||
true <- Pbkdf2.checkpw(password, db_user.password_hash) do | |||
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do | |||
{:ok, db_user} | |||
else | |||
_ -> {:error, dgettext("errors", "Invalid password.")} | |||
@@ -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)) | |||
@@ -47,6 +47,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
require Logger | |||
@rate_limited_relations_actions ~w(follow unfollow)a | |||
@rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status | |||
post_status delete_status)a | |||
@@ -62,9 +64,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
when action in ~w(fav_status unfav_status)a | |||
) | |||
plug( | |||
RateLimiter, | |||
{:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions | |||
) | |||
plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) | |||
plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) | |||
plug(RateLimiter, :app_account_creation when action == :account_register) | |||
plug(RateLimiter, :search when action in [:search, :search2, :account_search]) | |||
plug(RateLimiter, :password_reset when action == :password_reset) | |||
@local_mastodon_name "Mastodon-Local" | |||
@@ -693,11 +702,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||
else | |||
{:error, reason} -> | |||
conn | |||
|> put_status(:bad_request) | |||
|> json(%{"error" => reason}) | |||
end | |||
end | |||
@@ -738,11 +742,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||
else | |||
{:error, reason} -> | |||
conn | |||
|> put_resp_content_type("application/json") | |||
|> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) | |||
end | |||
end | |||
@@ -881,7 +880,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), | |||
with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), | |||
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do | |||
q = from(u in User, where: u.ap_id in ^likes) | |||
users = Repo.all(q) | |||
@@ -895,7 +894,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), | |||
with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), | |||
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do | |||
q = from(u in User, where: u.ap_id in ^announces) | |||
users = Repo.all(q) | |||
@@ -1068,9 +1067,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
end | |||
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do | |||
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do | |||
notifications = | |||
if Map.has_key?(params, "notifications"), | |||
do: params["notifications"] in [true, "True", "true", "1"], | |||
else: true | |||
with %User{} = muted <- User.get_cached_by_id(id), | |||
{:ok, muter} <- User.mute(muter, muted) do | |||
{:ok, muter} <- User.mute(muter, muted, notifications) do | |||
conn | |||
|> put_view(AccountView) | |||
|> render("relationship.json", %{user: muter, target: muted}) | |||
@@ -1646,6 +1650,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
render_error(conn, :not_found, "Record not found") | |||
end | |||
def errors(conn, {:error, error_message}) do | |||
conn | |||
|> put_status(:bad_request) | |||
|> json(%{error: error_message}) | |||
end | |||
def errors(conn, _) do | |||
conn | |||
|> put_status(:internal_server_error) | |||
@@ -1807,6 +1817,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
end | |||
def password_reset(conn, params) do | |||
nickname_or_email = params["email"] || params["nickname"] | |||
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do | |||
conn | |||
|> put_status(:no_content) | |||
|> json("") | |||
else | |||
{:error, "unknown user"} -> | |||
send_resp(conn, :not_found, "") | |||
{:error, _} -> | |||
send_resp(conn, :bad_request, "") | |||
end | |||
end | |||
def try_render(conn, target, params) | |||
when is_binary(target) do | |||
case render(conn, target, params) do | |||
@@ -51,8 +51,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do | |||
following: User.following?(user, target), | |||
followed_by: User.following?(target, user), | |||
blocking: User.blocks?(user, target), | |||
blocked_by: User.blocks?(target, user), | |||
muting: User.mutes?(user, target), | |||
muting_notifications: false, | |||
muting_notifications: User.muted_notifications?(user, target), | |||
subscribing: User.subscribed_to?(user, target), | |||
requested: requested, | |||
domain_blocking: false, | |||
@@ -136,6 +137,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do | |||
|> maybe_put_notification_settings(user, opts[:for]) | |||
|> maybe_put_settings_store(user, opts[:for], opts) | |||
|> maybe_put_chat_token(user, opts[:for], opts) | |||
|> maybe_put_activation_status(user, opts[:for]) | |||
end | |||
defp username_from_nickname(string) when is_binary(string) do | |||
@@ -196,6 +198,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do | |||
defp maybe_put_notification_settings(data, _, _), do: data | |||
defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do | |||
Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated) | |||
end | |||
defp maybe_put_activation_status(data, _, _), do: data | |||
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href | |||
defp image_url(_), do: nil | |||
end |
@@ -382,7 +382,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do | |||
%{ | |||
# Mastodon uses separate ids for polls, but an object can't have | |||
# more than one poll embedded so object id is fine | |||
id: object.id, | |||
id: to_string(object.id), | |||
expires_at: Utils.to_masto_date(end_time), | |||
expired: expired, | |||
multiple: multiple, | |||
@@ -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,13 +27,20 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do | |||
end | |||
end | |||
def filename_matches(has_filename, path, url) do | |||
filename = url |> MediaProxy.filename() | |||
def filename_matches(%{"filename" => _} = _, path, url) do | |||
filename = MediaProxy.filename(url) | |||
if has_filename && filename && Path.basename(path) != filename do | |||
if filename && does_not_match(path, filename) do | |||
{:wrong_filename, filename} | |||
else | |||
:ok | |||
end | |||
end | |||
def filename_matches(_, _, _), do: :ok | |||
defp does_not_match(path, filename) do | |||
basename = Path.basename(path) | |||
basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename | |||
end | |||
end |
@@ -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} | |||
@@ -587,7 +587,7 @@ defmodule Pleroma.Web.Router do | |||
end | |||
end | |||
pipeline :ap_relay do | |||
pipeline :ap_service_actor do | |||
plug(:accepts, ["activity+json", "json"]) | |||
end | |||
@@ -664,8 +664,17 @@ defmodule Pleroma.Web.Router do | |||
end | |||
scope "/relay", Pleroma.Web.ActivityPub do | |||
pipe_through(:ap_relay) | |||
pipe_through(:ap_service_actor) | |||
get("/", ActivityPubController, :relay) | |||
post("/inbox", ActivityPubController, :inbox) | |||
end | |||
scope "/internal/fetch", Pleroma.Web.ActivityPub do | |||
pipe_through(:ap_service_actor) | |||
get("/", ActivityPubController, :internal_fetch) | |||
post("/inbox", ActivityPubController, :inbox) | |||
end | |||
scope "/", Pleroma.Web.ActivityPub do | |||
@@ -692,6 +701,8 @@ defmodule Pleroma.Web.Router do | |||
get("/web/login", MastodonAPIController, :login) | |||
delete("/auth/sign_out", MastodonAPIController, :logout) | |||
post("/auth/password", MastodonAPIController, :password_reset) | |||
scope [] do | |||
pipe_through(:oauth_read_or_public) | |||
get("/web/*path", MastodonAPIController, :index) | |||
@@ -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) | |||
@@ -7,10 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do | |||
require Logger | |||
alias Comeonin.Pbkdf2 | |||
alias Pleroma.Activity | |||
alias Pleroma.Emoji | |||
alias Pleroma.Notification | |||
alias Pleroma.Plugs.AuthenticationPlug | |||
alias Pleroma.User | |||
alias Pleroma.Web | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
@@ -96,7 +96,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do | |||
name = followee.nickname | |||
with %User{} = user <- User.get_cached_by_nickname(username), | |||
true <- Pbkdf2.checkpw(password, user.password_hash), | |||
true <- AuthenticationPlug.checkpw(password, user.password_hash), | |||
%User{} = _followed <- User.get_cached_by_id(id), | |||
{:ok, follower} <- User.follow(user, followee), | |||
{:ok, _activity} <- ActivityPub.follow(follower, followee) do | |||
@@ -221,6 +221,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do | |||
user | |||
|> UserEmail.password_reset_email(token_record.token) | |||
|> Mailer.deliver_async() | |||
{:ok, :enqueued} | |||
else | |||
false -> | |||
{:error, "bad user identifier"} | |||
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||
require Logger | |||
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset) | |||
plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline]) | |||
action_fallback(:errors) | |||
@@ -192,6 +193,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||
end | |||
def notifications(%{assigns: %{user: user}} = conn, params) do | |||
params = | |||
if Map.has_key?(params, "with_muted") do | |||
Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"]) | |||
else | |||
params | |||
end | |||
notifications = Notification.for_user(user, params) | |||
conn | |||
@@ -430,6 +438,12 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do | |||
json_response(conn, :no_content, "") | |||
else | |||
{:error, "unknown user"} -> | |||
send_resp(conn, :not_found, "") | |||
{:error, _} -> | |||
send_resp(conn, :bad_request, "") | |||
end | |||
end | |||
@@ -11,10 +11,6 @@ defmodule Pleroma.Web.UploaderController do | |||
process_callback(conn, :global.whereis_name({Uploader, upload_path}), params) | |||
end | |||
def callbacks(conn, _) do | |||
render_error(conn, :bad_request, "bad request") | |||
end | |||
defp process_callback(conn, pid, params) when is_pid(pid) do | |||
send(pid, {Uploader, self(), conn, params}) | |||
@@ -32,7 +32,7 @@ defmodule Pleroma.Web.WebFinger do | |||
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do | |||
host = Pleroma.Web.Endpoint.host() | |||
regex = ~r/(acct:)?(?<username>\w+)@#{host}/ | |||
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ | |||
with %{"username" => username} <- Regex.named_captures(regex, resource), | |||
%User{} = user <- User.get_cached_by_nickname(username) do | |||
@@ -14,7 +14,7 @@ defmodule Pleroma.Mixfile do | |||
aliases: aliases(), | |||
deps: deps(), | |||
test_coverage: [tool: ExCoveralls], | |||
preferred_cli_env: ["coveralls.html": :test], | |||
# Docs | |||
name: "Pleroma", | |||
homepage_url: "https://pleroma.social/", | |||
@@ -95,6 +95,7 @@ defmodule Pleroma.Mixfile do | |||
defp deps do | |||
[ | |||
{:phoenix, "~> 1.4.8"}, | |||
{:tzdata, "~> 1.0"}, | |||
{:plug_cowboy, "~> 2.0"}, | |||
{:phoenix_pubsub, "~> 1.1"}, | |||
{:phoenix_ecto, "~> 4.0"}, | |||
@@ -137,7 +138,7 @@ defmodule Pleroma.Mixfile do | |||
ref: "95e8188490e97505c56636c1379ffdf036c1fdde"}, | |||
{:http_signatures, | |||
git: "https://git.pleroma.social/pleroma/http_signatures.git", | |||
ref: "9789401987096ead65646b52b5a2ca6bf52fc531"}, | |||
ref: "a2a5982fa167fb1352fbd518ce6b606ba233a989"}, | |||
{:pleroma_job_queue, "~> 0.2.0"}, | |||
{:telemetry, "~> 0.3"}, | |||
{:prometheus_ex, "~> 3.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"}, | |||
@@ -38,7 +38,7 @@ | |||
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, | |||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "9789401987096ead65646b52b5a2ca6bf52fc531", [ref: "9789401987096ead65646b52b5a2ca6bf52fc531"]}, | |||
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "a2a5982fa167fb1352fbd518ce6b606ba233a989", [ref: "a2a5982fa167fb1352fbd518ce6b606ba233a989"]}, | |||
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, | |||
@@ -82,9 +82,9 @@ | |||
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, | |||
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, | |||
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, | |||
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, | |||
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"}, | |||
@@ -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 | |||
@@ -134,4 +150,34 @@ defmodule Pleroma.Object.FetcherTest do | |||
assert object.id != object_two.id | |||
end | |||
end | |||
describe "signed fetches" do | |||
test_with_mock "it signs fetches when configured to do so", | |||
Pleroma.Signature, | |||
[:passthrough], | |||
[] do | |||
option = Pleroma.Config.get([:activitypub, :sign_object_fetches]) | |||
Pleroma.Config.put([:activitypub, :sign_object_fetches], true) | |||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") | |||
assert called(Pleroma.Signature.sign(:_, :_)) | |||
Pleroma.Config.put([:activitypub, :sign_object_fetches], option) | |||
end | |||
test_with_mock "it doesn't sign fetches when not configured to do so", | |||
Pleroma.Signature, | |||
[:passthrough], | |||
[] do | |||
option = Pleroma.Config.get([:activitypub, :sign_object_fetches]) | |||
Pleroma.Config.put([:activitypub, :sign_object_fetches], false) | |||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") | |||
refute called(Pleroma.Signature.sign(:_, :_)) | |||
Pleroma.Config.put([:activitypub, :sign_object_fetches], option) | |||
end | |||
end | |||
end |
@@ -54,4 +54,29 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do | |||
assert conn == ret_conn | |||
end | |||
describe "checkpw/2" do | |||
test "check pbkdf2 hash" do | |||
hash = | |||
"$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A" | |||
assert AuthenticationPlug.checkpw("test-password", hash) | |||
refute AuthenticationPlug.checkpw("test-password1", hash) | |||
end | |||
test "check sha512-crypt hash" do | |||
hash = | |||
"$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" | |||
assert AuthenticationPlug.checkpw("password", hash) | |||
refute AuthenticationPlug.checkpw("password1", hash) | |||
end | |||
test "it returns false when hash invalid" do | |||
hash = | |||
"psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" | |||
refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash) | |||
end | |||
end | |||
end |
@@ -5,7 +5,6 @@ | |||
defmodule Pleroma.ReverseProxyTest do | |||
use Pleroma.Web.ConnCase, async: true | |||
import ExUnit.CaptureLog | |||
import ExUnit.CaptureLog | |||
import Mox | |||
alias Pleroma.ReverseProxy | |||
alias Pleroma.ReverseProxy.ClientMock | |||
@@ -0,0 +1,103 @@ | |||
# 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 | |||
} | |||
defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\"" | |||
defp make_fake_conn(key_id), | |||
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} | |||
describe "fetch_public_key/1" do | |||
test "it returns key" do | |||
expected_result = {:ok, @rsa_public_key} | |||
user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}}) | |||
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result | |||
end | |||
test "it returns error when not found user" do | |||
assert Signature.fetch_public_key(make_fake_conn("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(make_fake_conn(user.ap_id)) == | |||
{:error, :error} | |||
end | |||
end | |||
describe "refetch_public_key/1" do | |||
test "it returns key" do | |||
ap_id = "https://mastodon.social/users/lambadalambda" | |||
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == | |||
{:ok, @rsa_public_key} | |||
end | |||
test "it returns error when not found user" do | |||
assert Signature.refetch_public_key(make_fake_conn("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 |
@@ -42,19 +42,18 @@ defmodule Pleroma.DataCase do | |||
:ok | |||
end | |||
def ensure_local_uploader(_context) do | |||
def ensure_local_uploader(context) do | |||
test_uploader = Map.get(context, :uploader, Pleroma.Uploaders.Local) | |||
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) | |||
filters = Pleroma.Config.get([Pleroma.Upload, :filters]) | |||
unless uploader == Pleroma.Uploaders.Local || filters != [] do | |||
Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) | |||
Pleroma.Config.put([Pleroma.Upload, :filters], []) | |||
Pleroma.Config.put([Pleroma.Upload, :uploader], test_uploader) | |||
Pleroma.Config.put([Pleroma.Upload, :filters], []) | |||
on_exit(fn -> | |||
Pleroma.Config.put([Pleroma.Upload, :uploader], uploader) | |||
Pleroma.Config.put([Pleroma.Upload, :filters], filters) | |||
end) | |||
end | |||
on_exit(fn -> | |||
Pleroma.Config.put([Pleroma.Upload, :uploader], uploader) | |||
Pleroma.Config.put([Pleroma.Upload, :filters], filters) | |||
end) | |||
:ok | |||
end | |||
@@ -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 |
@@ -0,0 +1,31 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Upload.Filter.DedupeTest do | |||
use Pleroma.DataCase | |||
alias Pleroma.Upload | |||
alias Pleroma.Upload.Filter.Dedupe | |||
@shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" | |||
test "adds shasum" do | |||
File.cp!( | |||
"test/fixtures/image.jpg", | |||
"test/fixtures/image_tmp.jpg" | |||
) | |||
upload = %Upload{ | |||
name: "an… image.jpg", | |||
content_type: "image/jpg", | |||
path: Path.absname("test/fixtures/image_tmp.jpg"), | |||
tempfile: Path.absname("test/fixtures/image_tmp.jpg") | |||
} | |||
assert { | |||
:ok, | |||
%Pleroma.Upload{id: @shasum, path: "#{@shasum}.jpg"} | |||
} = Dedupe.filter(upload) | |||
end | |||
end |
@@ -0,0 +1,44 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Upload.Filter.MogrifunTest do | |||
use Pleroma.DataCase | |||
import Mock | |||
alias Pleroma.Upload | |||
alias Pleroma.Upload.Filter | |||
test "apply mogrify filter" do | |||
File.cp!( | |||
"test/fixtures/image.jpg", | |||
"test/fixtures/image_tmp.jpg" | |||
) | |||
upload = %Upload{ | |||
name: "an… image.jpg", | |||
content_type: "image/jpg", | |||
path: Path.absname("test/fixtures/image_tmp.jpg"), | |||
tempfile: Path.absname("test/fixtures/image_tmp.jpg") | |||
} | |||
task = | |||
Task.async(fn -> | |||
assert_receive {:apply_filter, {}}, 4_000 | |||
end) | |||
with_mocks([ | |||
{Mogrify, [], | |||
[ | |||
open: fn _f -> %Mogrify.Image{} end, | |||
custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, | |||
custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, | |||
save: fn _f, _o -> :ok end | |||
]} | |||
]) do | |||
assert Filter.Mogrifun.filter(upload) == :ok | |||
end | |||
Task.await(task) | |||
end | |||
end |
@@ -0,0 +1,51 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Upload.Filter.MogrifyTest do | |||
use Pleroma.DataCase | |||
import Mock | |||
alias Pleroma.Config | |||
alias Pleroma.Upload | |||
alias Pleroma.Upload.Filter | |||
setup do | |||
filter = Config.get([Filter.Mogrify, :args]) | |||
on_exit(fn -> | |||
Config.put([Filter.Mogrify, :args], filter) | |||
end) | |||
end | |||
test "apply mogrify filter" do | |||
Config.put([Filter.Mogrify, :args], [{"tint", "40"}]) | |||
File.cp!( | |||
"test/fixtures/image.jpg", | |||
"test/fixtures/image_tmp.jpg" | |||
) | |||
upload = %Upload{ | |||
name: "an… image.jpg", | |||
content_type: "image/jpg", | |||
path: Path.absname("test/fixtures/image_tmp.jpg"), | |||
tempfile: Path.absname("test/fixtures/image_tmp.jpg") | |||
} | |||
task = | |||
Task.async(fn -> | |||
assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 | |||
end) | |||
with_mock Mogrify, | |||
open: fn _f -> %Mogrify.Image{} end, | |||
custom: fn _m, _a -> :ok end, | |||
custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, | |||
save: fn _f, _o -> :ok end do | |||
assert Filter.Mogrify.filter(upload) == :ok | |||
end | |||
Task.await(task) | |||
end | |||
end |
@@ -0,0 +1,39 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Upload.FilterTest do | |||
use Pleroma.DataCase | |||
alias Pleroma.Config | |||
alias Pleroma.Upload.Filter | |||
setup do | |||
custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text]) | |||
on_exit(fn -> | |||
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename) | |||
end) | |||
end | |||
test "applies filters" do | |||
Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") | |||
File.cp!( | |||
"test/fixtures/image.jpg", | |||
"test/fixtures/image_tmp.jpg" | |||
) | |||
upload = %Pleroma.Upload{ | |||
name: "an… image.jpg", | |||
content_type: "image/jpg", | |||
path: Path.absname("test/fixtures/image_tmp.jpg"), | |||
tempfile: Path.absname("test/fixtures/image_tmp.jpg") | |||
} | |||
assert Filter.filter([], upload) == {:ok, upload} | |||
assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload) | |||
assert upload.name == "custom-file.png" | |||
end | |||
end |
@@ -3,9 +3,96 @@ | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.UploadTest do | |||
alias Pleroma.Upload | |||
use Pleroma.DataCase | |||
alias Pleroma.Upload | |||
alias Pleroma.Uploaders.Uploader | |||
@upload_file %Plug.Upload{ | |||
content_type: "image/jpg", | |||
path: Path.absname("test/fixtures/image_tmp.jpg"), | |||
filename: "image.jpg" | |||
} | |||
defmodule TestUploaderBase do | |||
def put_file(%{path: path} = _upload, module_name) do | |||
task_pid = | |||
Task.async(fn -> | |||
:timer.sleep(10) | |||
{Uploader, path} | |||
|> :global.whereis_name() | |||
|> send({Uploader, self(), {:test}, %{}}) | |||
assert_receive {Uploader, {:test}}, 4_000 | |||
end) | |||
Agent.start(fn -> task_pid end, name: module_name) | |||
:wait_callback | |||
end | |||
end | |||
describe "Tried storing a file when http callback response success result" do | |||
defmodule TestUploaderSuccess do | |||
def http_callback(conn, _params), | |||
do: {:ok, conn, {:file, "post-process-file.jpg"}} | |||
def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) | |||
end | |||
setup do: [uploader: TestUploaderSuccess] | |||
setup [:ensure_local_uploader] | |||
test "it returns file" do | |||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") | |||
assert Upload.store(@upload_file) == | |||
{:ok, | |||
%{ | |||
"name" => "image.jpg", | |||
"type" => "Document", | |||
"url" => [ | |||
%{ | |||
"href" => "http://localhost:4001/media/post-process-file.jpg", | |||
"mediaType" => "image/jpeg", | |||
"type" => "Link" | |||
} | |||
] | |||
}} | |||
Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) | |||
end | |||
end | |||
describe "Tried storing a file when http callback response error" do | |||
defmodule TestUploaderError do | |||
def http_callback(conn, _params), do: {:error, conn, "Errors"} | |||
def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) | |||
end | |||
setup do: [uploader: TestUploaderError] | |||
setup [:ensure_local_uploader] | |||
test "it returns error" do | |||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") | |||
assert Upload.store(@upload_file) == {:error, "Errors"} | |||
Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end)) | |||
end | |||
end | |||
describe "Tried storing a file when http callback doesn't response by timeout" do | |||
defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback)) | |||
setup do: [uploader: TestUploader] | |||
setup [:ensure_local_uploader] | |||
test "it returns error" do | |||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") | |||
assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"} | |||
end | |||
end | |||
describe "Storing a file with the Local uploader" do | |||
setup [:ensure_local_uploader] | |||
@@ -687,10 +687,12 @@ defmodule Pleroma.UserTest do | |||
muted_user = insert(:user) | |||
refute User.mutes?(user, muted_user) | |||
refute User.muted_notifications?(user, muted_user) | |||
{:ok, user} = User.mute(user, muted_user) | |||
assert User.mutes?(user, muted_user) | |||
assert User.muted_notifications?(user, muted_user) | |||
end | |||
test "it unmutes users" do | |||
@@ -701,6 +703,20 @@ defmodule Pleroma.UserTest do | |||
{:ok, user} = User.unmute(user, muted_user) | |||
refute User.mutes?(user, muted_user) | |||
refute User.muted_notifications?(user, muted_user) | |||
end | |||
test "it mutes user without notifications" do | |||
user = insert(:user) | |||
muted_user = insert(:user) | |||
refute User.mutes?(user, muted_user) | |||
refute User.muted_notifications?(user, muted_user) | |||
{:ok, user} = User.mute(user, muted_user, false) | |||
assert User.mutes?(user, muted_user) | |||
refute User.muted_notifications?(user, muted_user) | |||
end | |||
end | |||
@@ -1294,4 +1310,21 @@ defmodule Pleroma.UserTest do | |||
assert following == 0 | |||
end | |||
end | |||
describe "is_internal_user?/1" do | |||
test "non-internal user returns false" do | |||
user = insert(:user) | |||
refute User.is_internal_user?(user) | |||
end | |||
test "user with no nickname returns true" do | |||
user = insert(:user, %{nickname: nil}) | |||
assert User.is_internal_user?(user) | |||
end | |||
test "user with internal-prefixed nickname returns true" do | |||
user = insert(:user, %{nickname: "internal.test"}) | |||
assert User.is_internal_user?(user) | |||
end | |||
end | |||
end |
@@ -48,6 +48,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do | |||
end | |||
end | |||
describe "/internal/fetch" do | |||
test "it returns the internal fetch user", %{conn: conn} do | |||
res = | |||
conn | |||
|> get(activity_pub_path(conn, :internal_fetch)) | |||
|> json_response(200) | |||
assert res["id"] =~ "/fetch" | |||
end | |||
end | |||
describe "/users/:nickname" do | |||
test "it returns a json representation of the user with accept application/json", %{ | |||
conn: conn | |||
@@ -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 | |||
@@ -0,0 +1,92 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do | |||
use Pleroma.DataCase | |||
alias Pleroma.Web.ActivityPub.MRF.MentionPolicy | |||
test "pass filter if allow list is empty" do | |||
Pleroma.Config.delete([:mrf_mention]) | |||
message = %{ | |||
"type" => "Create", | |||
"to" => ["https://example.com/ok"], | |||
"cc" => ["https://example.com/blocked"] | |||
} | |||
assert MentionPolicy.filter(message) == {:ok, message} | |||
end | |||
describe "allow" do | |||
test "empty" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create" | |||
} | |||
assert MentionPolicy.filter(message) == {:ok, message} | |||
end | |||
test "to" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create", | |||
"to" => ["https://example.com/ok"] | |||
} | |||
assert MentionPolicy.filter(message) == {:ok, message} | |||
end | |||
test "cc" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create", | |||
"cc" => ["https://example.com/ok"] | |||
} | |||
assert MentionPolicy.filter(message) == {:ok, message} | |||
end | |||
test "both" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create", | |||
"to" => ["https://example.com/ok"], | |||
"cc" => ["https://example.com/ok2"] | |||
} | |||
assert MentionPolicy.filter(message) == {:ok, message} | |||
end | |||
end | |||
describe "deny" do | |||
test "to" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create", | |||
"to" => ["https://example.com/blocked"] | |||
} | |||
assert MentionPolicy.filter(message) == {:reject, nil} | |||
end | |||
test "cc" do | |||
Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) | |||
message = %{ | |||
"type" => "Create", | |||
"to" => ["https://example.com/ok"], | |||
"cc" => ["https://example.com/blocked"] | |||
} | |||
assert MentionPolicy.filter(message) == {:reject, nil} | |||
end | |||
end | |||
end |
@@ -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 | |||
@@ -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 | |||
describe "GET /api/pleroma/admin/users/:nickname/statuses" do | |||
@@ -129,6 +129,37 @@ defmodule Pleroma.Web.CommonAPITest do | |||
}) | |||
end) | |||
end | |||
test "it allows to address a list" do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("foo", user) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) | |||
assert activity.data["bcc"] == [list.ap_id] | |||
assert activity.recipients == [list.ap_id, user.ap_id] | |||
assert activity.data["listMessage"] == list.ap_id | |||
end | |||
test "it returns error when status is empty and no attachments" do | |||
user = insert(:user) | |||
assert {:error, "Cannot post an empty status without attachments"} = | |||
CommonAPI.post(user, %{"status" => ""}) | |||
end | |||
test "it returns error when character limit is exceeded" do | |||
limit = Pleroma.Config.get([:instance, :limit]) | |||
Pleroma.Config.put([:instance, :limit], 5) | |||
user = insert(:user) | |||
assert {:error, "The status is over the character limit"} = | |||
CommonAPI.post(user, %{"status" => "foobar"}) | |||
Pleroma.Config.put([:instance, :limit], limit) | |||
end | |||
end | |||
describe "reactions" do | |||
@@ -346,6 +377,20 @@ defmodule Pleroma.Web.CommonAPITest do | |||
end | |||
end | |||
describe "unfollow/2" do | |||
test "also unsubscribes a user" do | |||
[follower, followed] = insert_pair(:user) | |||
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed) | |||
{:ok, followed} = User.subscribe(follower, followed) | |||
assert User.subscribed_to?(follower, followed) | |||
{:ok, follower} = CommonAPI.unfollow(follower, followed) | |||
refute User.subscribed_to?(follower, followed) | |||
end | |||
end | |||
describe "accept_follow_request/2" do | |||
test "after acceptance, it sets all existing pending follow request states to 'accept'" do | |||
user = insert(:user, info: %{locked: true}) | |||
@@ -387,4 +432,23 @@ defmodule Pleroma.Web.CommonAPITest do | |||
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" | |||
end | |||
end | |||
describe "vote/3" do | |||
test "does not allow to vote twice" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{ | |||
"status" => "Am I cute?", | |||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20} | |||
}) | |||
object = Object.normalize(activity) | |||
{:ok, _, object} = CommonAPI.vote(other_user, object, [0]) | |||
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) | |||
end | |||
end | |||
end |
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do | |||
alias Pleroma.Web.Endpoint | |||
use Pleroma.DataCase | |||
import ExUnit.CaptureLog | |||
import Pleroma.Factory | |||
@public_address "https://www.w3.org/ns/activitystreams#Public" | |||
@@ -202,7 +203,9 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do | |||
expected = "" | |||
assert Utils.date_to_asctime(date) == expected | |||
assert capture_log(fn -> | |||
assert Utils.date_to_asctime(date) == expected | |||
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" | |||
end | |||
test "when date is a Unix timestamp" do | |||
@@ -210,13 +213,23 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do | |||
expected = "" | |||
assert Utils.date_to_asctime(date) == expected | |||
assert capture_log(fn -> | |||
assert Utils.date_to_asctime(date) == expected | |||
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" | |||
end | |||
test "when date is nil" do | |||
expected = "" | |||
assert Utils.date_to_asctime(nil) == expected | |||
assert capture_log(fn -> | |||
assert Utils.date_to_asctime(nil) == expected | |||
end) =~ "[warn] Date in wrong format, must be ISO 8601" | |||
end | |||
test "when date is a random string" do | |||
assert capture_log(fn -> | |||
assert Utils.date_to_asctime("foo") == "" | |||
end) =~ "[warn] Date foo in wrong format, must be ISO 8601" | |||
end | |||
end | |||
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
use Pleroma.DataCase | |||
import Pleroma.Factory | |||
alias Pleroma.User | |||
alias Pleroma.Web.CommonAPI | |||
alias Pleroma.Web.MastodonAPI.AccountView | |||
test "Represent a user account" do | |||
@@ -152,6 +153,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
assert expected == AccountView.render("account.json", %{user: user}) | |||
end | |||
test "Represent a deactivated user for an admin" do | |||
admin = insert(:user, %{info: %{is_admin: true}}) | |||
deactivated_user = insert(:user, %{info: %{deactivated: true}}) | |||
represented = AccountView.render("account.json", %{user: deactivated_user, for: admin}) | |||
assert represented[:pleroma][:deactivated] == true | |||
end | |||
test "Represent a smaller mention" do | |||
user = insert(:user) | |||
@@ -165,28 +173,90 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
assert expected == AccountView.render("mention.json", %{user: user}) | |||
end | |||
test "represent a relationship" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
describe "relationship" do | |||
test "represent a relationship for the following and followed user" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, user} = User.follow(user, other_user) | |||
{:ok, user} = User.block(user, other_user) | |||
{:ok, user} = User.follow(user, other_user) | |||
{:ok, other_user} = User.follow(other_user, user) | |||
{:ok, other_user} = User.subscribe(user, other_user) | |||
{:ok, user} = User.mute(user, other_user, true) | |||
{:ok, user} = CommonAPI.hide_reblogs(user, other_user) | |||
expected = %{ | |||
id: to_string(other_user.id), | |||
following: false, | |||
followed_by: false, | |||
blocking: true, | |||
muting: false, | |||
muting_notifications: false, | |||
subscribing: false, | |||
requested: false, | |||
domain_blocking: false, | |||
showing_reblogs: true, | |||
endorsed: false | |||
} | |||
expected = %{ | |||
id: to_string(other_user.id), | |||
following: true, | |||
followed_by: true, | |||
blocking: false, | |||
blocked_by: false, | |||
muting: true, | |||
muting_notifications: true, | |||
subscribing: true, | |||
requested: false, | |||
domain_blocking: false, | |||
showing_reblogs: false, | |||
endorsed: false | |||
} | |||
assert expected == | |||
AccountView.render("relationship.json", %{user: user, target: other_user}) | |||
end | |||
test "represent a relationship for the blocking and blocked user" do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, user} = User.follow(user, other_user) | |||
{:ok, other_user} = User.subscribe(user, other_user) | |||
{:ok, user} = User.block(user, other_user) | |||
{:ok, other_user} = User.block(other_user, user) | |||
expected = %{ | |||
id: to_string(other_user.id), | |||
following: false, | |||
followed_by: false, | |||
blocking: true, | |||
blocked_by: true, | |||
muting: false, | |||
muting_notifications: false, | |||
subscribing: false, | |||
requested: false, | |||
domain_blocking: false, | |||
showing_reblogs: true, | |||
endorsed: false | |||
} | |||
assert expected == | |||
AccountView.render("relationship.json", %{user: user, target: other_user}) | |||
end | |||
test "represent a relationship for the user with a pending follow request" do | |||
user = insert(:user) | |||
other_user = insert(:user, %{info: %User.Info{locked: true}}) | |||
{:ok, user, other_user, _} = CommonAPI.follow(user, other_user) | |||
user = User.get_cached_by_id(user.id) | |||
other_user = User.get_cached_by_id(other_user.id) | |||
expected = %{ | |||
id: to_string(other_user.id), | |||
following: false, | |||
followed_by: false, | |||
blocking: false, | |||
blocked_by: false, | |||
muting: false, | |||
muting_notifications: false, | |||
subscribing: false, | |||
requested: true, | |||
domain_blocking: false, | |||
showing_reblogs: true, | |||
endorsed: false | |||
} | |||
assert expected == AccountView.render("relationship.json", %{user: user, target: other_user}) | |||
assert expected == | |||
AccountView.render("relationship.json", %{user: user, target: other_user}) | |||
end | |||
end | |||
test "represent an embedded relationship" do | |||
@@ -240,6 +310,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
following: false, | |||
followed_by: false, | |||
blocking: true, | |||
blocked_by: false, | |||
subscribing: false, | |||
muting: false, | |||
muting_notifications: false, | |||
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
import Pleroma.Factory | |||
import ExUnit.CaptureLog | |||
import Tesla.Mock | |||
import Swoosh.TestAssertions | |||
@image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" | |||
@@ -35,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user = insert(:user) | |||
following = insert(:user) | |||
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) | |||
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) | |||
conn = | |||
conn | |||
@@ -58,7 +59,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
following = insert(:user) | |||
capture_log(fn -> | |||
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) | |||
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) | |||
{:ok, [_activity]} = | |||
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") | |||
@@ -1004,8 +1005,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
test "list timeline", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."}) | |||
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) | |||
{:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."}) | |||
{:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) | |||
{:ok, list} = Pleroma.List.create("name", user) | |||
{:ok, list} = Pleroma.List.follow(list, other_user) | |||
@@ -1022,10 +1023,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity_one} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) | |||
{:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) | |||
{:ok, _activity_two} = | |||
TwitterAPI.create_status(other_user, %{ | |||
CommonAPI.post(other_user, %{ | |||
"status" => "Marisa is cute.", | |||
"visibility" => "private" | |||
}) | |||
@@ -1049,8 +1050,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, [_notification]} = Notification.create_notifications(activity) | |||
@@ -1072,8 +1072,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, [notification]} = Notification.create_notifications(activity) | |||
@@ -1095,8 +1094,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, [notification]} = Notification.create_notifications(activity) | |||
@@ -1112,8 +1110,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) | |||
{:ok, [_notification]} = Notification.create_notifications(activity) | |||
@@ -1274,6 +1271,71 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
result = json_response(conn_res, 200) | |||
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result | |||
end | |||
test "doesn't see notifications after muting user with notifications", %{conn: conn} do | |||
user = insert(:user) | |||
user2 = insert(:user) | |||
{:ok, _, _, _} = CommonAPI.follow(user, user2) | |||
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) | |||
conn = assign(conn, :user, user) | |||
conn = get(conn, "/api/v1/notifications") | |||
assert length(json_response(conn, 200)) == 1 | |||
{:ok, user} = User.mute(user, user2) | |||
conn = assign(build_conn(), :user, user) | |||
conn = get(conn, "/api/v1/notifications") | |||
assert json_response(conn, 200) == [] | |||
end | |||
test "see notifications after muting user without notifications", %{conn: conn} do | |||
user = insert(:user) | |||
user2 = insert(:user) | |||
{:ok, _, _, _} = CommonAPI.follow(user, user2) | |||
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) | |||
conn = assign(conn, :user, user) | |||
conn = get(conn, "/api/v1/notifications") | |||
assert length(json_response(conn, 200)) == 1 | |||
{:ok, user} = User.mute(user, user2, false) | |||
conn = assign(build_conn(), :user, user) | |||
conn = get(conn, "/api/v1/notifications") | |||
assert length(json_response(conn, 200)) == 1 | |||
end | |||
test "see notifications after muting user with notifications and with_muted parameter", %{ | |||
conn: conn | |||
} do | |||
user = insert(:user) | |||
user2 = insert(:user) | |||
{:ok, _, _, _} = CommonAPI.follow(user, user2) | |||
{:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) | |||
conn = assign(conn, :user, user) | |||
conn = get(conn, "/api/v1/notifications") | |||
assert length(json_response(conn, 200)) == 1 | |||
{:ok, user} = User.mute(user, user2) | |||
conn = assign(build_conn(), :user, user) | |||
conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) | |||
assert length(json_response(conn, 200)) == 1 | |||
end | |||
end | |||
describe "reblogging" do | |||
@@ -1330,6 +1392,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert to_string(activity.id) == id | |||
end | |||
test "returns 400 error when activity is not exist", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/foo/reblog") | |||
assert json_response(conn, 400) == %{"error" => "Could not repeat"} | |||
end | |||
end | |||
describe "unreblogging" do | |||
@@ -1348,6 +1421,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert to_string(activity.id) == id | |||
end | |||
test "returns 400 error when activity is not exist", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/foo/unreblog") | |||
assert json_response(conn, 400) == %{"error" => "Could not unrepeat"} | |||
end | |||
end | |||
describe "favoriting" do | |||
@@ -1366,16 +1450,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert to_string(activity.id) == id | |||
end | |||
test "returns 500 for a wrong id", %{conn: conn} do | |||
test "returns 400 error for a wrong id", %{conn: conn} do | |||
user = insert(:user) | |||
resp = | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/1/favourite") | |||
|> json_response(500) | |||
assert resp == "Something went wrong" | |||
assert json_response(conn, 400) == %{"error" => "Could not favorite"} | |||
end | |||
end | |||
@@ -1396,6 +1479,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert to_string(activity.id) == id | |||
end | |||
test "returns 400 error for a wrong id", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/1/unfavourite") | |||
assert json_response(conn, 400) == %{"error" => "Could not unfavorite"} | |||
end | |||
end | |||
describe "user timelines" do | |||
@@ -1466,10 +1560,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
media = | |||
TwitterAPI.upload(file, user, "json") | |||
|> Poison.decode!() | |||
|> Jason.decode!() | |||
{:ok, image_post} = | |||
TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]}) | |||
CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]}) | |||
conn = | |||
conn | |||
@@ -1792,7 +1886,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
following = insert(:user) | |||
capture_log(fn -> | |||
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"}) | |||
{:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"}) | |||
{:ok, [_activity]} = | |||
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") | |||
@@ -2105,25 +2199,52 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert %{"error" => "Record not found"} = json_response(conn_res, 404) | |||
end | |||
test "muting / unmuting a user", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
describe "mute/unmute" do | |||
test "with notifications", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/mute") | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/mute") | |||
assert %{"id" => _id, "muting" => true} = json_response(conn, 200) | |||
response = json_response(conn, 200) | |||
user = User.get_cached_by_id(user.id) | |||
assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response | |||
user = User.get_cached_by_id(user.id) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/unmute") | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/unmute") | |||
response = json_response(conn, 200) | |||
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response | |||
end | |||
test "without notifications", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) | |||
response = json_response(conn, 200) | |||
assert %{"id" => _id, "muting" => false} = json_response(conn, 200) | |||
assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response | |||
user = User.get_cached_by_id(user.id) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
|> post("/api/v1/accounts/#{other_user.id}/unmute") | |||
response = json_response(conn, 200) | |||
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response | |||
end | |||
end | |||
test "subscribing / unsubscribing to a user", %{conn: conn} do | |||
@@ -2524,7 +2645,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
insert(:user, %{local: false, nickname: "u@peer1.com"}) | |||
insert(:user, %{local: false, nickname: "u@peer2.com"}) | |||
{:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"}) | |||
{:ok, _} = CommonAPI.post(user, %{"status" => "cofe"}) | |||
# Stats should count users with missing or nil `info.deactivated` value | |||
user = User.get_cached_by_id(user.id) | |||
@@ -2617,6 +2738,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> json_response(200) | |||
end | |||
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do | |||
{:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"}) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/#{dm.id}/pin") | |||
assert json_response(conn, 400) == %{"error" => "Could not pin"} | |||
end | |||
test "unpin status", %{conn: conn, user: user, activity: activity} do | |||
{:ok, _} = CommonAPI.pin(activity.id, user) | |||
@@ -2636,6 +2768,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> json_response(200) | |||
end | |||
test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/1/unpin") | |||
assert json_response(conn, 400) == %{"error" => "Could not unpin"} | |||
end | |||
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do | |||
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) | |||
@@ -2810,6 +2951,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> json_response(200) | |||
end | |||
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do | |||
{:ok, _} = CommonAPI.add_mute(user, activity) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/statuses/#{activity.id}/mute") | |||
assert json_response(conn, 400) == %{"error" => "conversation is already muted"} | |||
end | |||
test "unmute conversation", %{conn: conn, user: user, activity: activity} do | |||
{:ok, _} = CommonAPI.add_mute(user, activity) | |||
@@ -2854,7 +3006,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> post("/api/v1/reports", %{ | |||
"account_id" => target_user.id, | |||
"status_ids" => [activity.id], | |||
"comment" => "bad status!" | |||
"comment" => "bad status!", | |||
"forward" => "false" | |||
}) | |||
|> json_response(200) | |||
end | |||
@@ -2887,6 +3040,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) | |||
|> json_response(400) | |||
end | |||
test "returns error when account is not exist", %{ | |||
conn: conn, | |||
reporter: reporter, | |||
activity: activity | |||
} do | |||
conn = | |||
conn | |||
|> assign(:user, reporter) | |||
|> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) | |||
assert json_response(conn, 400) == %{"error" => "Account not found"} | |||
end | |||
end | |||
describe "link headers" do | |||
@@ -3246,7 +3412,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
user2 = insert(:user) | |||
user3 = insert(:user) | |||
{:ok, replied_to} = TwitterAPI.create_status(user1, %{"status" => "cofe"}) | |||
{:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"}) | |||
# Reply to status from another user | |||
conn1 = | |||
@@ -3411,7 +3577,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> get("/api/v1/polls/#{object.id}") | |||
response = json_response(conn, 200) | |||
id = object.id | |||
id = to_string(object.id) | |||
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response | |||
end | |||
@@ -3511,5 +3677,186 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
total_items == 1 | |||
end) | |||
end | |||
test "does not allow choice index to be greater than options count", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{ | |||
"status" => "Am I cute?", | |||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20} | |||
}) | |||
object = Object.normalize(activity) | |||
conn = | |||
conn | |||
|> assign(:user, other_user) | |||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) | |||
assert json_response(conn, 422) == %{"error" => "Invalid indices"} | |||
end | |||
test "returns 404 error when object is not exist", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> assign(:user, user) | |||
|> post("/api/v1/polls/1/votes", %{"choices" => [0]}) | |||
assert json_response(conn, 404) == %{"error" => "Record not found"} | |||
end | |||
test "returns 404 when poll is private and not available for user", %{conn: conn} do | |||
user = insert(:user) | |||
other_user = insert(:user) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{ | |||
"status" => "Am I cute?", | |||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}, | |||
"visibility" => "private" | |||
}) | |||
object = Object.normalize(activity) | |||
conn = | |||
conn | |||
|> assign(:user, other_user) | |||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) | |||
assert json_response(conn, 404) == %{"error" => "Record not found"} | |||
end | |||
end | |||
describe "GET /api/v1/statuses/:id/favourited_by" do | |||
setup do | |||
user = insert(:user) | |||
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
[conn: conn, activity: activity] | |||
end | |||
test "returns users who have favorited the status", %{conn: conn, activity: activity} do | |||
other_user = insert(:user) | |||
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user) | |||
response = | |||
conn | |||
|> get("/api/v1/statuses/#{activity.id}/favourited_by") | |||
|> json_response(:ok) | |||
[%{"id" => id}] = response | |||
assert id == other_user.id | |||
end | |||
test "returns empty array when status has not been favorited yet", %{ | |||
conn: conn, | |||
activity: activity | |||
} do | |||
response = | |||
conn | |||
|> get("/api/v1/statuses/#{activity.id}/favourited_by") | |||
|> json_response(:ok) | |||
assert Enum.empty?(response) | |||
end | |||
end | |||
describe "GET /api/v1/statuses/:id/reblogged_by" do | |||
setup do | |||
user = insert(:user) | |||
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
[conn: conn, activity: activity] | |||
end | |||
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do | |||
other_user = insert(:user) | |||
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user) | |||
response = | |||
conn | |||
|> get("/api/v1/statuses/#{activity.id}/reblogged_by") | |||
|> json_response(:ok) | |||
[%{"id" => id}] = response | |||
assert id == other_user.id | |||
end | |||
test "returns empty array when status has not been reblogged yet", %{ | |||
conn: conn, | |||
activity: activity | |||
} do | |||
response = | |||
conn | |||
|> get("/api/v1/statuses/#{activity.id}/reblogged_by") | |||
|> json_response(:ok) | |||
assert Enum.empty?(response) | |||
end | |||
end | |||
describe "POST /auth/password, with valid parameters" do | |||
setup %{conn: conn} do | |||
user = insert(:user) | |||
conn = post(conn, "/auth/password?email=#{user.email}") | |||
%{conn: conn, user: user} | |||
end | |||
test "it returns 204", %{conn: conn} do | |||
assert json_response(conn, :no_content) | |||
end | |||
test "it creates a PasswordResetToken record for user", %{user: user} do | |||
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) | |||
assert token_record | |||
end | |||
test "it sends an email to user", %{user: user} do | |||
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) | |||
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) | |||
notify_email = Pleroma.Config.get([:instance, :notify_email]) | |||
instance_name = Pleroma.Config.get([:instance, :name]) | |||
assert_email_sent( | |||
from: {instance_name, notify_email}, | |||
to: {user.name, user.email}, | |||
html_body: email.html_body | |||
) | |||
end | |||
end | |||
describe "POST /auth/password, with invalid parameters" do | |||
setup do | |||
user = insert(:user) | |||
{:ok, user: user} | |||
end | |||
test "it returns 404 when user is not found", %{conn: conn, user: user} do | |||
conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") | |||
assert conn.status == 404 | |||
assert conn.resp_body == "" | |||
end | |||
test "it returns 400 when user is not local", %{conn: conn, user: user} do | |||
{:ok, user} = Repo.update(Changeset.change(user, local: false)) | |||
conn = post(conn, "/auth/password?email=#{user.email}") | |||
assert conn.status == 400 | |||
assert conn.resp_body == "" | |||
end | |||
end | |||
end |
@@ -423,7 +423,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do | |||
expected = %{ | |||
emojis: [], | |||
expired: false, | |||
id: object.id, | |||
id: to_string(object.id), | |||
multiple: false, | |||
options: [ | |||
%{title: "absolutely!", votes_count: 0}, | |||
@@ -541,4 +541,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do | |||
assert result[:reblog][:account][:pleroma][:relationship] == | |||
AccountView.render("relationship.json", %{user: user, target: user}) | |||
end | |||
test "visibility/list" do | |||
user = insert(:user) | |||
{:ok, list} = Pleroma.List.create("foo", user) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) | |||
status = StatusView.render("status.json", activity: activity) | |||
assert status.visibility == "list" | |||
end | |||
end |
@@ -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,39 @@ defmodule Pleroma.MediaProxyTest do | |||
test "filename_matches preserves the encoded or decoded path" do | |||
assert MediaProxyController.filename_matches( | |||
true, | |||
%{"filename" => "/Hello world.jpg"}, | |||
"/Hello world.jpg", | |||
"http://pleroma.social/Hello world.jpg" | |||
) == :ok | |||
assert MediaProxyController.filename_matches( | |||
true, | |||
%{"filename" => "/Hello%20world.jpg"}, | |||
"/Hello%20world.jpg", | |||
"http://pleroma.social/Hello%20world.jpg" | |||
) == :ok | |||
assert MediaProxyController.filename_matches( | |||
true, | |||
%{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, | |||
"/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", | |||
"http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" | |||
) == :ok | |||
assert MediaProxyController.filename_matches( | |||
%{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, | |||
"/my%2Flong%2Furl%2F2019%2F07%2FS.jp", | |||
"http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" | |||
) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} | |||
end | |||
test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do | |||
# conn.request_path will return encoded url | |||
request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" | |||
assert MediaProxyController.filename_matches( | |||
true, | |||
request_path, | |||
"https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" | |||
) == :ok | |||
end | |||
test "uses the configured base_url" do |
@@ -521,6 +521,38 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do | |||
for: current_user | |||
}) | |||
end | |||
test "muted user", %{conn: conn, user: current_user} do | |||
other_user = insert(:user) | |||
{:ok, current_user} = User.mute(current_user, other_user) | |||
{:ok, _activity} = | |||
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) | |||
conn = | |||
conn | |||
|> with_credentials(current_user.nickname, "test") | |||
|> get("/api/qvitter/statuses/notifications.json") | |||
assert json_response(conn, 200) == [] | |||
end | |||
test "muted user with with_muted parameter", %{conn: conn, user: current_user} do | |||
other_user = insert(:user) | |||
{:ok, current_user} = User.mute(current_user, other_user) | |||
{:ok, _activity} = | |||
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) | |||
conn = | |||
conn | |||
|> with_credentials(current_user.nickname, "test") | |||
|> get("/api/qvitter/statuses/notifications.json", %{"with_muted" => "true"}) | |||
assert length(json_response(conn, 200)) == 1 | |||
end | |||
end | |||
describe "POST /api/qvitter/statuses/notifications/read" do | |||
@@ -1084,15 +1116,17 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do | |||
describe "POST /api/account/password_reset, with invalid parameters" do | |||
setup [:valid_user] | |||
test "it returns 500 when user is not found", %{conn: conn, user: user} do | |||
test "it returns 404 when user is not found", %{conn: conn, user: user} do | |||
conn = post(conn, "/api/account/password_reset?email=nonexisting_#{user.email}") | |||
assert json_response(conn, :internal_server_error) | |||
assert conn.status == 404 | |||
assert conn.resp_body == "" | |||
end | |||
test "it returns 500 when user is not local", %{conn: conn, user: user} do | |||
test "it returns 400 when user is not local", %{conn: conn, user: user} do | |||
{:ok, user} = Repo.update(Changeset.change(user, local: false)) | |||
conn = post(conn, "/api/account/password_reset?email=#{user.email}") | |||
assert json_response(conn, :internal_server_error) | |||
assert conn.status == 400 | |||
assert conn.resp_body == "" | |||
end | |||
end | |||
@@ -0,0 +1,43 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.UploaderControllerTest do | |||
use Pleroma.Web.ConnCase | |||
alias Pleroma.Uploaders.Uploader | |||
describe "callback/2" do | |||
test "it returns 400 response when process callback isn't alive", %{conn: conn} do | |||
res = | |||
conn | |||
|> post(uploader_path(conn, :callback, "test-path")) | |||
assert res.status == 400 | |||
assert res.resp_body == "{\"error\":\"bad request\"}" | |||
end | |||
test "it returns success result", %{conn: conn} do | |||
task = | |||
Task.async(fn -> | |||
receive do | |||
{Uploader, pid, conn, _params} -> | |||
conn = | |||
conn | |||
|> put_status(:ok) | |||
|> Phoenix.Controller.json(%{upload_path: "test-path"}) | |||
send(pid, {Uploader, conn}) | |||
end | |||
end) | |||
:global.register_name({Uploader, "test-path"}, task.pid) | |||
res = | |||
conn | |||
|> post(uploader_path(conn, :callback, "test-path")) | |||
|> json_response(200) | |||
assert res == %{"upload_path" => "test-path"} | |||
end | |||
end | |||
end |