diff options
111 files changed, 4767 insertions, 770 deletions
| diff --git a/.gitignore b/.gitignore index 04c61ede7..774893b35 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ erl_crash.dump  # Editor config  /.vscode/ + +# Prevent committing docs files +/priv/static/doc/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbdf59f65..c07f1a5d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,5 @@  image: elixir:1.8.1 -services: -  - name: postgres:9.6.2 -    command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] -  variables:    POSTGRES_DB: pleroma_test    POSTGRES_USER: postgres @@ -17,58 +13,60 @@ cache:            - deps            - _build  stages: -  - lint +  - build    - test -  - analysis -  - docs_build -  - docs_deploy +  - deploy  before_script:    - mix local.hex --force    - mix local.rebar --force -  - mix deps.get -  - mix compile --force -  - mix ecto.create -  - mix ecto.migrate - -lint: -  stage: lint -  script: -    - mix format --check-formatted - -unit-testing: -  stage: test -  script: -    - mix test --trace --preload-modules -analysis: -  stage: analysis +build: +  stage: build    script: -    - mix credo --strict --only=warnings,todo,fixme,consistency,readability +  - mix deps.get +  - mix compile --force -docs_build: -  stage: docs_build -  services: +docs-build: +  stage: build    only:    - master@pleroma/pleroma    - develop@pleroma/pleroma    variables:      MIX_ENV: dev -  before_script: -    - mix local.hex --force -    - mix local.rebar --force +  script:      - mix deps.get      - mix compile -  script:      - mix docs    artifacts:      paths:        - priv/static/doc -docs_deploy: -  stage: docs_deploy -  image: alpine:3.9 +unit-testing: +  stage: test    services: +  - name: postgres:9.6.2 +    command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] +  script: +    - mix ecto.create +    - mix ecto.migrate +    - mix test --trace --preload-modules + +lint: +  stage: test +  script: +    - mix format --check-formatted + +analysis: +  stage: test +  script: +    - mix deps.get +    - mix credo --strict --only=warnings,todo,fixme,consistency,readability + + +docs-deploy: +  stage: deploy +  image: alpine:3.9    only:    - master@pleroma/pleroma    - develop@pleroma/pleroma diff --git a/CC-BY-NC-ND-4.0 b/CC-BY-NC-ND-4.0 new file mode 100644 index 000000000..486544290 --- /dev/null +++ b/CC-BY-NC-ND-4.0 @@ -0,0 +1,403 @@ +Attribution-NonCommercial-NoDerivatives 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + +     Considerations for licensors: Our public licenses are +     intended for use by those authorized to give the public +     permission to use material in ways otherwise restricted by +     copyright and certain other rights. Our licenses are +     irrevocable. Licensors should read and understand the terms +     and conditions of the license they choose before applying it. +     Licensors should also secure all rights necessary before +     applying our licenses so that the public can reuse the +     material as expected. Licensors should clearly mark any +     material not subject to the license. This includes other CC- +     licensed material, or material used under an exception or +     limitation to copyright. More considerations for licensors: +	wiki.creativecommons.org/Considerations_for_licensors + +     Considerations for the public: By using one of our public +     licenses, a licensor grants the public permission to use the +     licensed material under specified terms and conditions. If +     the licensor's permission is not necessary for any reason--for +     example, because of any applicable exception or limitation to +     copyright--then that use is not regulated by the license. Our +     licenses grant only permissions under copyright and certain +     other rights that a licensor has authority to grant. Use of +     the licensed material may still be restricted for other +     reasons, including because others have copyright or other +     rights in the material. A licensor may make special requests, +     such as asking that all changes be marked or described. +     Although not required by our licenses, you are encouraged to +     respect those requests where reasonable. More considerations +     for the public:  +	wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 +International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-NoDerivatives 4.0 International Public +License ("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + +  a. Adapted Material means material subject to Copyright and Similar +     Rights that is derived from or based upon the Licensed Material +     and in which the Licensed Material is translated, altered, +     arranged, transformed, or otherwise modified in a manner requiring +     permission under the Copyright and Similar Rights held by the +     Licensor. For purposes of this Public License, where the Licensed +     Material is a musical work, performance, or sound recording, +     Adapted Material is always produced where the Licensed Material is +     synched in timed relation with a moving image. + +  b. Copyright and Similar Rights means copyright and/or similar rights +     closely related to copyright including, without limitation, +     performance, broadcast, sound recording, and Sui Generis Database +     Rights, without regard to how the rights are labeled or +     categorized. For purposes of this Public License, the rights +     specified in Section 2(b)(1)-(2) are not Copyright and Similar +     Rights. + +  c. Effective Technological Measures means those measures that, in the +     absence of proper authority, may not be circumvented under laws +     fulfilling obligations under Article 11 of the WIPO Copyright +     Treaty adopted on December 20, 1996, and/or similar international +     agreements. + +  d. Exceptions and Limitations means fair use, fair dealing, and/or +     any other exception or limitation to Copyright and Similar Rights +     that applies to Your use of the Licensed Material. + +  e. Licensed Material means the artistic or literary work, database, +     or other material to which the Licensor applied this Public +     License. + +  f. Licensed Rights means the rights granted to You subject to the +     terms and conditions of this Public License, which are limited to +     all Copyright and Similar Rights that apply to Your use of the +     Licensed Material and that the Licensor has authority to license. + +  g. Licensor means the individual(s) or entity(ies) granting rights +     under this Public License. + +  h. NonCommercial means not primarily intended for or directed towards +     commercial advantage or monetary compensation. For purposes of +     this Public License, the exchange of the Licensed Material for +     other material subject to Copyright and Similar Rights by digital +     file-sharing or similar means is NonCommercial provided there is +     no payment of monetary compensation in connection with the +     exchange. + +  i. Share means to provide material to the public by any means or +     process that requires permission under the Licensed Rights, such +     as reproduction, public display, public performance, distribution, +     dissemination, communication, or importation, and to make material +     available to the public including in ways that members of the +     public may access the material from a place and at a time +     individually chosen by them. + +  j. Sui Generis Database Rights means rights other than copyright +     resulting from Directive 96/9/EC of the European Parliament and of +     the Council of 11 March 1996 on the legal protection of databases, +     as amended and/or succeeded, as well as other essentially +     equivalent rights anywhere in the world. + +  k. You means the individual or entity exercising the Licensed Rights +     under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + +  a. License grant. + +       1. Subject to the terms and conditions of this Public License, +          the Licensor hereby grants You a worldwide, royalty-free, +          non-sublicensable, non-exclusive, irrevocable license to +          exercise the Licensed Rights in the Licensed Material to: + +            a. reproduce and Share the Licensed Material, in whole or +               in part, for NonCommercial purposes only; and + +            b. produce and reproduce, but not Share, Adapted Material +               for NonCommercial purposes only. + +       2. Exceptions and Limitations. For the avoidance of doubt, where +          Exceptions and Limitations apply to Your use, this Public +          License does not apply, and You do not need to comply with +          its terms and conditions. + +       3. Term. The term of this Public License is specified in Section +          6(a). + +       4. Media and formats; technical modifications allowed. The +          Licensor authorizes You to exercise the Licensed Rights in +          all media and formats whether now known or hereafter created, +          and to make technical modifications necessary to do so. The +          Licensor waives and/or agrees not to assert any right or +          authority to forbid You from making technical modifications +          necessary to exercise the Licensed Rights, including +          technical modifications necessary to circumvent Effective +          Technological Measures. For purposes of this Public License, +          simply making modifications authorized by this Section 2(a) +          (4) never produces Adapted Material. + +       5. Downstream recipients. + +            a. Offer from the Licensor -- Licensed Material. Every +               recipient of the Licensed Material automatically +               receives an offer from the Licensor to exercise the +               Licensed Rights under the terms and conditions of this +               Public License. + +            b. No downstream restrictions. You may not offer or impose +               any additional or different terms or conditions on, or +               apply any Effective Technological Measures to, the +               Licensed Material if doing so restricts exercise of the +               Licensed Rights by any recipient of the Licensed +               Material. + +       6. No endorsement. Nothing in this Public License constitutes or +          may be construed as permission to assert or imply that You +          are, or that Your use of the Licensed Material is, connected +          with, or sponsored, endorsed, or granted official status by, +          the Licensor or others designated to receive attribution as +          provided in Section 3(a)(1)(A)(i). + +  b. Other rights. + +       1. Moral rights, such as the right of integrity, are not +          licensed under this Public License, nor are publicity, +          privacy, and/or other similar personality rights; however, to +          the extent possible, the Licensor waives and/or agrees not to +          assert any such rights held by the Licensor to the limited +          extent necessary to allow You to exercise the Licensed +          Rights, but not otherwise. + +       2. Patent and trademark rights are not licensed under this +          Public License. + +       3. To the extent possible, the Licensor waives any right to +          collect royalties from You for the exercise of the Licensed +          Rights, whether directly or through a collecting society +          under any voluntary or waivable statutory or compulsory +          licensing scheme. In all other cases the Licensor expressly +          reserves any right to collect such royalties, including when +          the Licensed Material is used other than for NonCommercial +          purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + +  a. Attribution. + +       1. If You Share the Licensed Material, You must: + +            a. retain the following if it is supplied by the Licensor +               with the Licensed Material: + +                 i. identification of the creator(s) of the Licensed +                    Material and any others designated to receive +                    attribution, in any reasonable manner requested by +                    the Licensor (including by pseudonym if +                    designated); + +                ii. a copyright notice; + +               iii. a notice that refers to this Public License; + +                iv. a notice that refers to the disclaimer of +                    warranties; + +                 v. a URI or hyperlink to the Licensed Material to the +                    extent reasonably practicable; + +            b. indicate if You modified the Licensed Material and +               retain an indication of any previous modifications; and + +            c. indicate the Licensed Material is licensed under this +               Public License, and include the text of, or the URI or +               hyperlink to, this Public License. + +          For the avoidance of doubt, You do not have permission under +          this Public License to Share Adapted Material. + +       2. You may satisfy the conditions in Section 3(a)(1) in any +          reasonable manner based on the medium, means, and context in +          which You Share the Licensed Material. For example, it may be +          reasonable to satisfy the conditions by providing a URI or +          hyperlink to a resource that includes the required +          information. + +       3. If requested by the Licensor, You must remove any of the +          information required by Section 3(a)(1)(A) to the extent +          reasonably practicable. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + +  a. for the avoidance of doubt, Section 2(a)(1) grants You the right +     to extract, reuse, reproduce, and Share all or a substantial +     portion of the contents of the database for NonCommercial purposes +     only and provided You do not Share Adapted Material; + +  b. if You include all or a substantial portion of the database +     contents in a database in which You have Sui Generis Database +     Rights, then the database in which You have Sui Generis Database +     Rights (but not its individual contents) is Adapted Material; and + +  c. You must comply with the conditions in Section 3(a) if You Share +     all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + +  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE +     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS +     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF +     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, +     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, +     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, +     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT +     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT +     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE +     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, +     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, +     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, +     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR +     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN +     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR +     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR +     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +  c. The disclaimer of warranties and limitation of liability provided +     above shall be interpreted in a manner that, to the extent +     possible, most closely approximates an absolute disclaimer and +     waiver of all liability. + + +Section 6 -- Term and Termination. + +  a. This Public License applies for the term of the Copyright and +     Similar Rights licensed here. However, if You fail to comply with +     this Public License, then Your rights under this Public License +     terminate automatically. + +  b. Where Your right to use the Licensed Material has terminated under +     Section 6(a), it reinstates: + +       1. automatically as of the date the violation is cured, provided +          it is cured within 30 days of Your discovery of the +          violation; or + +       2. upon express reinstatement by the Licensor. + +     For the avoidance of doubt, this Section 6(b) does not affect any +     right the Licensor may have to seek remedies for Your violations +     of this Public License. + +  c. For the avoidance of doubt, the Licensor may also offer the +     Licensed Material under separate terms or conditions or stop +     distributing the Licensed Material at any time; however, doing so +     will not terminate this Public License. + +  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public +     License. + + +Section 7 -- Other Terms and Conditions. + +  a. The Licensor shall not be bound by any additional or different +     terms or conditions communicated by You unless expressly agreed. + +  b. Any arrangements, understandings, or agreements regarding the +     Licensed Material not stated herein are separate from and +     independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + +  a. For the avoidance of doubt, this Public License does not, and +     shall not be interpreted to, reduce, limit, restrict, or impose +     conditions on any use of the Licensed Material that could lawfully +     be made without permission under this Public License. + +  b. To the extent possible, if any provision of this Public License is +     deemed unenforceable, it shall be automatically reformed to the +     minimum extent necessary to make it enforceable. If the provision +     cannot be reformed, it shall be severed from this Public License +     without affecting the enforceability of the remaining terms and +     conditions. + +  c. No term or condition of this Public License will be waived and no +     failure to comply consented to unless expressly agreed to by the +     Licensor. + +  d. Nothing in this Public License constitutes or may be interpreted +     as a limitation upon, or waiver of, any privileges and immunities +     that apply to the Licensor or You, including from the legal +     processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/CC-BY-SA-4.0 b/CC-BY-SA-4.0 new file mode 100644 index 000000000..4681ab80f --- /dev/null +++ b/CC-BY-SA-4.0 @@ -0,0 +1,427 @@ +Attribution-ShareAlike 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-ShareAlike 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-ShareAlike 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. Adapter's License means the license You apply to Your Copyright +     and Similar Rights in Your contributions to Adapted Material in +     accordance with the terms and conditions of this Public License. + +  c. BY-SA Compatible License means a license listed at +     creativecommons.org/compatiblelicenses, approved by Creative +     Commons as essentially the equivalent of this Public License. + +  d. 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. + +  e. 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. + +  f. 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. + +  g. License Elements means the license attributes listed in the name +     of a Creative Commons Public License. The License Elements of this +     Public License are Attribution and ShareAlike. + +  h. Licensed Material means the artistic or literary work, database, +     or other material to which the Licensor applied this Public +     License. + +  i. 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. + +  j. Licensor means the individual(s) or entity(ies) granting rights +     under this Public License. + +  k. 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. + +  l. 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. + +  m. 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; and + +            b. produce, reproduce, and Share Adapted Material. + +       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. Additional offer from the Licensor -- Adapted Material. +               Every recipient of Adapted Material from You +               automatically receives an offer from the Licensor to +               exercise the Licensed Rights in the Adapted Material +               under the conditions of the Adapter's License You apply. + +            c. 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. + + +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 (including in modified +          form), 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. + +       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. + +  b. ShareAlike. + +     In addition to the conditions in Section 3(a), if You Share +     Adapted Material You produce, the following conditions also apply. + +       1. The Adapter's License You apply must be a Creative Commons +          license with the same License Elements, this version or +          later, or a BY-SA Compatible License. + +       2. You must include the text of, or the URI or hyperlink to, the +          Adapter's License You apply. You may satisfy this condition +          in any reasonable manner based on the medium, means, and +          context in which You Share Adapted Material. + +       3. You may not offer or impose any additional or different terms +          or conditions on, or apply any Effective Technological +          Measures to, Adapted Material that restrict exercise of the +          rights granted under the Adapter's License You apply. + + +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; + +  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, +     including for purposes of Section 3(b); and + +  c. You must comply with the conditions in Section 3(a) if You Share +     all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + +  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE +     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS +     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF +     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, +     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, +     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, +     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT +     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT +     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE +     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, +     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, +     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, +     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR +     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN +     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR +     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR +     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +  c. The disclaimer of warranties and limitation of liability provided +     above shall be interpreted in a manner that, to the extent +     possible, most closely approximates an absolute disclaimer and +     waiver of all liability. + + +Section 6 -- Term and Termination. + +  a. This Public License applies for the term of the Copyright and +     Similar Rights licensed here. However, if You fail to comply with +     this Public License, then Your rights under this Public License +     terminate automatically. + +  b. Where Your right to use the Licensed Material has terminated under +     Section 6(a), it reinstates: + +       1. automatically as of the date the violation is cured, provided +          it is cured within 30 days of Your discovery of the +          violation; or + +       2. upon express reinstatement by the Licensor. + +     For the avoidance of doubt, this Section 6(b) does not affect any +     right the Licensor may have to seek remedies for Your violations +     of this Public License. + +  c. For the avoidance of doubt, the Licensor may also offer the +     Licensed Material under separate terms or conditions or stop +     distributing the Licensed Material at any time; however, doing so +     will not terminate this Public License. + +  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public +     License. + + +Section 7 -- Other Terms and Conditions. + +  a. The Licensor shall not be bound by any additional or different +     terms or conditions communicated by You unless expressly agreed. + +  b. Any arrangements, understandings, or agreements regarding the +     Licensed Material not stated herein are separate from and +     independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + +  a. For the avoidance of doubt, this Public License does not, and +     shall not be interpreted to, reduce, limit, restrict, or impose +     conditions on any use of the Licensed Material that could lawfully +     be made without permission under this Public License. + +  b. To the extent possible, if any provision of this Public License is +     deemed unenforceable, it shall be automatically reformed to the +     minimum extent necessary to make it enforceable. If the provision +     cannot be reformed, it shall be severed from this Public License +     without affecting the enforceability of the remaining terms and +     conditions. + +  c. No term or condition of this Public License will be waived and no +     failure to comply consented to unless expressly agreed to by the +     Licensor. + +  d. Nothing in this Public License constitutes or may be interpreted +     as a limitation upon, or waiver of, any privileges and immunities +     that apply to the Licensor or You, including from the legal +     processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..ceec519ae --- /dev/null +++ b/COPYING @@ -0,0 +1,48 @@ +Unless otherwise stated this repository is copyright © 2017-2019 +Pleroma Authors <https://pleroma.social/>, and is distributed under +The GNU Affero General Public License Version 3, you should have received a +copy of the license file as AGPL-3. + +--- + +The following files are copyright © 2019 shitposter.club, and are distributed +under the Creative Commons Attribution-ShareAlike 4.0 International license, +you should have received a copy of the license file as CC-BY-SA-4.0. + +priv/static/images/pleroma-fox-tan.png +priv/static/images/pleroma-fox-tan-smol.png +priv/static/images/pleroma-tan.png + +--- + +The following files are copyright © 2017-2019 Pleroma Authors +<https://pleroma.social/>, and are distributed under the Creative Commons +Attribution-ShareAlike 4.0 International license, you should have received +a copy of the license file as CC-BY-SA-4.0. + +priv/static/images/avi.png +priv/static/images/banner.png +priv/static/instance/thumbnail.jpeg + +--- + +All photos published on Unsplash can be used for free. You can use them for +commercial and noncommercial purposes. You do not need to ask permission from +or provide credit to the photographer or Unsplash, although it is appreciated +when possible. + +More precisely, Unsplash grants you an irrevocable, nonexclusive, worldwide +copyright license to download, copy, modify, distribute, perform, and use +photos from Unsplash for free, including for commercial purposes, without +permission from or attributing the photographer or Unsplash. This license +does not include the right to compile photos from Unsplash to replicate +a similar or competing service. + +priv/static/images/city.jpg + +--- + +The files present under the priv/static/finmoji directory are copyright +Finland <https://finland.fi/emoji/>, and are distributed under the Creative +Commons Attribution-NonCommercial-NoDerivatives 4.0 International license, you +should have received a copy of the license file as CC-BY-NC-ND-4.0. diff --git a/config/config.exs b/config/config.exs index dccf7b263..3462a37f7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,6 +8,10 @@ use Mix.Config  # General application configuration  config :pleroma, ecto_repos: [Pleroma.Repo] +config :pleroma, Pleroma.Repo, +  types: Pleroma.PostgresTypes, +  telemetry_event: [Pleroma.Repo.Instrumenter] +  config :pleroma, Pleroma.Captcha,    enabled: false,    seconds_valid: 60, @@ -54,7 +58,13 @@ config :pleroma, Pleroma.Uploaders.MDII,    cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi",    files: "https://mdii.sakura.ne.jp" -config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"] +config :pleroma, :emoji, +  shortcode_globs: ["/emoji/custom/**/*.png"], +  groups: [ +    # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md` +    Finmoji: "/finmoji/128px/*-128.png", +    Custom: ["/emoji/*.png", "/emoji/custom/*.png"] +  ]  config :pleroma, :uri_schemes,    valid_schemes: [ @@ -87,6 +97,7 @@ websocket_config = [  # Configures the endpoint  config :pleroma, Pleroma.Web.Endpoint, +  instrumenters: [Pleroma.Web.Endpoint.Instrumenter],    url: [host: "localhost"],    http: [      dispatch: [ @@ -118,6 +129,11 @@ config :logger, :ex_syslogger,    format: "$metadata[$level] $message",    metadata: [:request_id] +config :quack, +  level: :warn, +  meta: [:all], +  webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE" +  config :mime, :types, %{    "application/xml" => ["xml"],    "application/xrd+xml" => ["xrd+xml"], @@ -351,7 +367,10 @@ config :pleroma, Pleroma.Web.Federator.RetryQueue,  config :pleroma_job_queue, :queues,    federator_incoming: 50,    federator_outgoing: 50, -  mailer: 10 +  web_push: 50, +  mailer: 10, +  transmogrifier: 20, +  scheduled_activities: 10  config :pleroma, :fetch_initial_posts,    enabled: false, @@ -378,8 +397,31 @@ config :pleroma, :ldap,    base: System.get_env("LDAP_BASE") || "dc=example,dc=com",    uid: System.get_env("LDAP_UID") || "cn" +oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") + +ueberauth_providers = +  for strategy <- oauth_consumer_strategies do +    strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}" +    strategy_module = String.to_atom(strategy_module_name) +    {String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}} +  end + +config :ueberauth, +       Ueberauth, +       base_path: "/oauth", +       providers: ueberauth_providers + +config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies +  config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Sendmail +config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics" + +config :pleroma, Pleroma.ScheduledActivity, +  daily_user_limit: 25, +  total_user_limit: 300, +  enabled: true +  # Import environment specific config. This must remain at the bottom  # of this file so it overrides the configuration defined above.  import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index f77bb9976..a7eb4b644 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -12,7 +12,6 @@ config :pleroma, Pleroma.Web.Endpoint,      protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]    ],    protocol: "http", -  secure_cookie_flag: false,    debug_errors: true,    code_reloader: true,    check_origin: false, diff --git a/config/emoji.txt b/config/emoji.txt index 7afacb09f..79246f239 100644 --- a/config/emoji.txt +++ b/config/emoji.txt @@ -1,5 +1,5 @@ -firefox, /emoji/Firefox.gif -blank, /emoji/blank.png +firefox, /emoji/Firefox.gif, Gif,Fun +blank, /emoji/blank.png, Fun  f_00b, /emoji/f_00b.png  f_00b11b, /emoji/f_00b11b.png  f_00b33b, /emoji/f_00b33b.png @@ -28,4 +28,3 @@ f_33b00b, /emoji/f_33b00b.png  f_33b22b, /emoji/f_33b22b.png  f_33h, /emoji/f_33h.png  f_33t, /emoji/f_33t.png - diff --git a/config/test.exs b/config/test.exs index 6a7b9067e..894fa8d3d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -50,6 +50,11 @@ config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock  config :pleroma_job_queue, disabled: true +config :pleroma, Pleroma.ScheduledActivity, +  daily_user_limit: 2, +  total_user_limit: 3, +  enabled: false +  try do    import_config "test.secret.exs"  rescue diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 53b68ffd4..86cacebb1 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -58,6 +58,26 @@ Authentication is required and the user must be an admin.    - `password`  - Response: User’s nickname +## `/api/pleroma/admin/user/follow` +### Make a user follow another user + +- Methods: `POST` +- Params: + - `follower`: The nickname of the follower + - `followed`: The nickname of the followed +- Response: + - "ok" + +## `/api/pleroma/admin/user/unfollow` +### Make a user unfollow another user + +- Methods: `POST` +- Params: + - `follower`: The nickname of the follower + - `followed`: The nickname of the followed +- Response: + - "ok" +  ## `/api/pleroma/admin/users/:nickname/toggle_activation`  ### Toggle user activation diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index d993d1383..215f43155 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -44,3 +44,9 @@ Has these additional fields under the `pleroma` object:  Has these additional fields under the `pleroma` object:  - `is_seen`: true if the notification was read by the user + +## POST `/api/v1/statuses` + +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. diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 478c9d874..2e8fb04d2 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -10,7 +10,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi  * Authentication: not required  * Params: none  * Response: JSON -* Example response: `{"kalsarikannit_f":"/finmoji/128px/kalsarikannit_f-128.png","perkele":"/finmoji/128px/perkele-128.png","blobdab":"/emoji/blobdab.png","happiness":"/finmoji/128px/happiness-128.png"}` +* Example response: `[{"kalsarikannit_f":{"tags":["Finmoji"],"image_url":"/finmoji/128px/kalsarikannit_f-128.png"}},{"perkele":{"tags":["Finmoji"],"image_url":"/finmoji/128px/perkele-128.png"}},{"blobdab":{"tags":["SomeTag"],"image_url":"/emoji/blobdab.png"}},"happiness":{"tags":["Finmoji"],"image_url":"/finmoji/128px/happiness-128.png"}}]`  * Note: Same data as Mastodon API’s `/api/v1/custom_emojis` but in a different format  ## `/api/pleroma/follow_import` @@ -27,14 +27,14 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi  * Method: `GET`  * Authentication: not required  * Params: none -* Response: Provider specific JSON, the only guaranteed parameter is `type`  +* Response: Provider specific JSON, the only guaranteed parameter is `type`  * Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint"}`  ## `/api/pleroma/delete_account`  ### Delete an account  * Method `POST`  * Authentication: required -* Params:  +* Params:      * `password`: user's password  * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise  * Example response: `{"error": "Invalid password."}` diff --git a/docs/api/prometheus.md b/docs/api/prometheus.md new file mode 100644 index 000000000..19c564e3c --- /dev/null +++ b/docs/api/prometheus.md @@ -0,0 +1,22 @@ +# Prometheus Metrics + +Pleroma includes support for exporting metrics via the [prometheus_ex](https://github.com/deadtrickster/prometheus.ex) library. + +## `/api/pleroma/app_metrics` +### Exports Prometheus application metrics +* Method: `GET` +* Authentication: not required +* Params: none +* Response: JSON + +## Grafana +### Config example +The following is a config example to use with [Grafana](https://grafana.com) + +``` +  - job_name: 'beam' +    metrics_path: /api/pleroma/app_metrics +    scheme: https +    static_configs: +    - targets: ['pleroma.soykaf.com'] +``` diff --git a/docs/config.md b/docs/config.md index 97a0e6ffa..b5ea58746 100644 --- a/docs/config.md +++ b/docs/config.md @@ -105,7 +105,7 @@ config :pleroma, Pleroma.Mailer,  * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)  ## :logger -* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog +* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack  An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed:  ``` @@ -128,6 +128,24 @@ config :logger, :ex_syslogger,  See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_syslogger’s documentation](https://hexdocs.pm/ex_syslogger/) +An example of logging info to local syslog, but warn to a Slack channel: +``` +config :logger, +  backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ], +  level: :info + +config :logger, :ex_syslogger, +  level: :info, +  ident: "pleroma", +  format: "$metadata[$level] $message" + +config :quack, +  level: :warn, +  meta: [:all], +  webhook_url: "https://hooks.slack.com/services/YOUR-API-KEY-HERE" +``` + +See the [Quack Github](https://github.com/azohra/quack) for more details  ## :frontend_configurations @@ -200,14 +218,14 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i    - `port`  * `url` - a list containing the configuration for generating urls, accepts    - `host` - the host without the scheme and a post (e.g `example.com`, not `https://example.com:2020`) -  - `scheme` - e.g `http`, `https`  +  - `scheme` - e.g `http`, `https`    - `port`    - `path`  **Important note**: if you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need -Example:  +Example:  ```elixir  config :pleroma, Pleroma.Web.Endpoint,    url: [host: "example.com", port: 2020, scheme: "https"], @@ -296,9 +314,13 @@ curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerando  [Pleroma Job Queue](https://git.pleroma.social/pleroma/pleroma_job_queue) configuration: a list of queues with maximum concurrent jobs.  Pleroma has the following queues: +  * `federator_outgoing` - Outgoing federation  * `federator_incoming` - Incoming federation  * `mailer` - Email sender, see [`Pleroma.Mailer`](#pleroma-mailer) +* `transmogrifier` - Transmogrifier +* `web_push` - Web push notifications +* `scheduled_activities` - Scheduled activities, see [`Pleroma.ScheduledActivities`](#pleromascheduledactivity)  Example: @@ -372,6 +394,17 @@ config :auto_linker,    ]  ``` +## Pleroma.ScheduledActivity + +* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`) +* `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) +* `enabled`: whether scheduled activities are sent to the job queue to be executed + +## Pleroma.Web.Auth.Authenticator + +* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator +* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication +  ## :ldap  Use LDAP for user authentication.  When a user logs in to the Pleroma @@ -390,7 +423,61 @@ Pleroma account will be created with the same name as the LDAP user name.  * `base`: LDAP base, e.g. "dc=example,dc=com"  * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" -## Pleroma.Web.Auth.Authenticator +## :auth -* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator -* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication +Authentication / authorization settings. + +* `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`.  +* `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`. +* `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. + +# OAuth consumer mode + +OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). +Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). + +Note: each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, +e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. +The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies. + +Note: each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies.   + +* For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback + +* For Facebook, [register an app](https://developers.facebook.com/apps), configure callback URL to https://<your_host>/oauth/facebook/callback, enable Facebook Login service at https://developers.facebook.com/apps/<app_id>/fb-login/settings/ + +* For Google, [register an app](https://console.developers.google.com), configure callback URL to https://<your_host>/oauth/google/callback + +* For Microsoft, [register an app](https://portal.azure.com), configure callback URL to https://<your_host>/oauth/microsoft/callback + +Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`, +per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables: + +``` +# Twitter +config :ueberauth, Ueberauth.Strategy.Twitter.OAuth, +  consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), +  consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET") + +# Facebook +config :ueberauth, Ueberauth.Strategy.Facebook.OAuth, +  client_id: System.get_env("FACEBOOK_APP_ID"), +  client_secret: System.get_env("FACEBOOK_APP_SECRET"), +  redirect_uri: System.get_env("FACEBOOK_REDIRECT_URI") + +# Google +config :ueberauth, Ueberauth.Strategy.Google.OAuth, +  client_id: System.get_env("GOOGLE_CLIENT_ID"), +  client_secret: System.get_env("GOOGLE_CLIENT_SECRET"), +  redirect_uri: System.get_env("GOOGLE_REDIRECT_URI") + +# Microsoft +config :ueberauth, Ueberauth.Strategy.Microsoft.OAuth, +  client_id: System.get_env("MICROSOFT_CLIENT_ID"), +  client_secret: System.get_env("MICROSOFT_CLIENT_SECRET") +   +config :ueberauth, Ueberauth, +  providers: [ +    microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]} +  ] +``` diff --git a/docs/config/custom_emoji.md b/docs/config/custom_emoji.md index e833d2080..419a7d0e2 100644 --- a/docs/config/custom_emoji.md +++ b/docs/config/custom_emoji.md @@ -11,8 +11,43 @@ image files (in `/priv/static/emoji/custom`): `happy.png` and `sad.png`  content of `config/custom_emoji.txt`:  ``` -happy, /emoji/custom/happy.png -sad, /emoji/custom/sad.png +happy, /emoji/custom/happy.png, Tag1,Tag2 +sad, /emoji/custom/sad.png, Tag1 +foo, /emoji/custom/foo.png  ```  The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon. + +## Emoji tags (groups) + +Default tags are set in `config.exs`. +```elixir +config :pleroma, :emoji, +  shortcode_globs: ["/emoji/custom/**/*.png"], +  groups: [ +    Finmoji: "/finmoji/128px/*-128.png", +    Custom: ["/emoji/*.png", "/emoji/custom/*.png"] +  ] +``` + +Order of the `groups` matters, so to override default tags just put your group on top of the list. E.g: +```elixir +config :pleroma, :emoji, +  shortcode_globs: ["/emoji/custom/**/*.png"], +  groups: [ +    "Finmoji special": "/finmoji/128px/a_trusted_friend-128.png", # special file +    "Cirno": "/emoji/custom/cirno*.png", # png files in /emoji/custom/ which start with `cirno` +    "Special group": "/emoji/custom/special_folder/*.png", # png files in /emoji/custom/special_folder/ +    "Another group": "/emoji/custom/special_folder/*/.png", # png files in /emoji/custom/special_folder/ subfolders +    Finmoji: "/finmoji/128px/*-128.png", +    Custom: ["/emoji/*.png", "/emoji/custom/*.png"] +  ] +``` + +Priority of tags assigns in emoji.txt and custom.txt: + +`tag in file > special group setting in config.exs > default setting in config.exs` + +Priority for globs: + +`special group setting in config.exs > default setting in config.exs` diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 1ba452275..8f8d86a11 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -81,6 +81,14 @@ defmodule Mix.Tasks.Pleroma.Instance do        email = Common.get_option(options, :admin_email, "What is your admin email address?") +      indexable = +        Common.get_option( +          options, +          :indexable, +          "Do you want search engines to index your site? (y/n)", +          "y" +        ) === "y" +        dbhost =          Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") @@ -142,6 +150,8 @@ defmodule Mix.Tasks.Pleroma.Instance do        Mix.shell().info("Writing #{psql_path}.")        File.write(psql_path, result_psql) +      write_robots_txt(indexable) +        Mix.shell().info(          "\n" <>            """ @@ -163,4 +173,28 @@ defmodule Mix.Tasks.Pleroma.Instance do        )      end    end + +  defp write_robots_txt(indexable) do +    robots_txt = +      EEx.eval_file( +        Path.expand("robots_txt.eex", __DIR__), +        indexable: indexable +      ) + +    static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + +    unless File.exists?(static_dir) do +      File.mkdir_p!(static_dir) +    end + +    robots_txt_path = Path.join(static_dir, "robots.txt") + +    if File.exists?(robots_txt_path) do +      File.cp!(robots_txt_path, "#{robots_txt_path}.bak") +      Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") +    end + +    File.write(robots_txt_path, robots_txt) +    Mix.shell().info("Writing #{robots_txt_path}.") +  end  end diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex new file mode 100644 index 000000000..1af3c47ee --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.eex @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: <%= if indexable, do: "", else: "/" %> diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index f6cca0d06..0d0bea8c0 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -6,7 +6,6 @@ defmodule Mix.Tasks.Pleroma.User do    use Mix.Task    import Ecto.Changeset    alias Mix.Tasks.Pleroma.Common -  alias Pleroma.Repo    alias Pleroma.User    @shortdoc "Manages Pleroma users" @@ -23,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.User do    - `--password PASSWORD` - the user's password    - `--moderator`/`--no-moderator` - whether the user is a moderator    - `--admin`/`--no-admin` - whether the user is an admin -  - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions  +  - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions    ## Generate an invite link. @@ -33,6 +32,10 @@ defmodule Mix.Tasks.Pleroma.User do        mix pleroma.user rm NICKNAME +  ## Delete the user's activities. + +      mix pleroma.user delete_activities NICKNAME +    ## Deactivate or activate the user's account.        mix pleroma.user toggle_activated NICKNAME @@ -202,7 +205,7 @@ defmodule Mix.Tasks.Pleroma.User do        {:ok, friends} = User.get_friends(user)        Enum.each(friends, fn friend -> -        user = Repo.get(User, user.id) +        user = User.get_by_id(user.id)          Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")          User.unfollow(user, friend) @@ -210,7 +213,7 @@ defmodule Mix.Tasks.Pleroma.User do        :timer.sleep(500) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        if Enum.empty?(user.following) do          Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") @@ -304,6 +307,18 @@ defmodule Mix.Tasks.Pleroma.User do      end    end +  def run(["delete_activities", nickname]) do +    Common.start_pleroma() + +    with %User{local: true} = user <- User.get_by_nickname(nickname) do +      User.delete_user_activities(user) +      Mix.shell().info("User #{nickname} statuses deleted.") +    else +      _ -> +        Mix.shell().error("No local user #{nickname}") +    end +  end +    defp set_moderator(user, value) do      info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex index 772c239a1..7afbc8751 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/PasswordResetToken.ex @@ -39,7 +39,7 @@ defmodule Pleroma.PasswordResetToken do    def reset_password(token, data) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- Repo.get(User, token.user_id), +         %User{} = user <- User.get_by_id(token.user_id),           {:ok, _user} <- User.reset_password(user, data),           {:ok, token} <- Repo.update(used_changeset(token)) do        {:ok, token} diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index bc3f8caba..ab8861b27 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Activity do      field(:data, :map)      field(:local, :boolean, default: true)      field(:actor, :string) -    field(:recipients, {:array, :string}) +    field(:recipients, {:array, :string}, default: [])      has_many(:notifications, Notification, on_delete: :delete_all)      # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 782d1d589..eeb415084 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Application do      import Cachex.Spec      Pleroma.Config.DeprecationWarnings.warn() +    setup_instrumenters()      # Define workers and child supervisors to be supervised      children = @@ -103,14 +104,15 @@ defmodule Pleroma.Application do            ],            id: :cachex_idem          ), -        worker(Pleroma.FlakeId, []) +        worker(Pleroma.FlakeId, []), +        worker(Pleroma.ScheduledActivityWorker, [])        ] ++          hackney_pool_children() ++          [            worker(Pleroma.Web.Federator.RetryQueue, []),            worker(Pleroma.Stats, []), -          worker(Pleroma.Web.Push, []), -          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary) +          worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init), +          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)          ] ++          streamer_child() ++          chat_child() ++ @@ -126,6 +128,24 @@ defmodule Pleroma.Application do      Supervisor.start_link(children, opts)    end +  defp setup_instrumenters do +    require Prometheus.Registry + +    :ok = +      :telemetry.attach( +        "prometheus-ecto", +        [:pleroma, :repo, :query], +        &Pleroma.Repo.Instrumenter.handle_event/4, +        %{} +      ) + +    Prometheus.Registry.register_collector(:prometheus_process_collector) +    Pleroma.Web.Endpoint.MetricsExporter.setup() +    Pleroma.Web.Endpoint.PipelineInstrumenter.setup() +    Pleroma.Web.Endpoint.Instrumenter.setup() +    Pleroma.Repo.Instrumenter.setup() +  end +    def enabled_hackney_pools do      [:media] ++        if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index 21507cd38..189faa15f 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -57,4 +57,8 @@ defmodule Pleroma.Config do    def delete(key) do      Application.delete_env(:pleroma, key)    end + +  def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) + +  def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []  end diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index f3f08cd9d..87c7f2cec 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -8,13 +8,19 @@ defmodule Pleroma.Emoji do      * the built-in Finmojis (if enabled in configuration),      * the files: `config/emoji.txt` and `config/custom_emoji.txt` -    * glob paths +    * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder    This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.    """    use GenServer + +  @type pattern :: Regex.t() | module() | String.t() +  @type patterns :: pattern() | [pattern()] +  @type group_patterns :: keyword(patterns()) +    @ets __MODULE__.Ets    @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] +  @groups Application.get_env(:pleroma, :emoji)[:groups]    @doc false    def start_link do @@ -73,13 +79,14 @@ defmodule Pleroma.Emoji do    end    defp load do +    finmoji_enabled = Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled) +    shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || [] +      emojis = -      (load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++ +      (load_finmoji(finmoji_enabled) ++           load_from_file("config/emoji.txt") ++           load_from_file("config/custom_emoji.txt") ++ -         load_from_globs( -           Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, []) -         )) +         load_from_globs(shortcode_globs))        |> Enum.reject(fn value -> value == nil end)      true = :ets.insert(@ets, emojis) @@ -151,9 +158,12 @@ defmodule Pleroma.Emoji do      "white_nights",      "woollysocks"    ] +    defp load_finmoji(true) do      Enum.map(@finmoji, fn finmoji -> -      {finmoji, "/finmoji/128px/#{finmoji}-128.png"} +      file_name = "/finmoji/128px/#{finmoji}-128.png" +      group = match_extra(@groups, file_name) +      {finmoji, file_name, to_string(group)}      end)    end @@ -172,8 +182,14 @@ defmodule Pleroma.Emoji do      |> Stream.map(&String.trim/1)      |> Stream.map(fn line ->        case String.split(line, ~r/,\s*/) do -        [name, file] -> {name, file} -        _ -> nil +        [name, file, tags] -> +          {name, file, tags} + +        [name, file] -> +          {name, file, to_string(match_extra(@groups, file))} + +        _ -> +          nil        end      end)      |> Enum.to_list() @@ -190,9 +206,40 @@ defmodule Pleroma.Emoji do        |> Enum.concat()      Enum.map(paths, fn path -> +      tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))        shortcode = Path.basename(path, Path.extname(path))        external_path = Path.join("/", Path.relative_to(path, static_path)) -      {shortcode, external_path} +      {shortcode, external_path, to_string(tag)} +    end) +  end + +  @doc """ +  Finds a matching group for the given emoji filename +  """ +  @spec match_extra(group_patterns(), String.t()) :: atom() | nil +  def match_extra(group_patterns, filename) do +    match_group_patterns(group_patterns, fn pattern -> +      case pattern do +        %Regex{} = regex -> Regex.match?(regex, filename) +        string when is_binary(string) -> filename == string +      end +    end) +  end + +  defp match_group_patterns(group_patterns, matcher) do +    Enum.find_value(group_patterns, fn {group, patterns} -> +      patterns = +        patterns +        |> List.wrap() +        |> Enum.map(fn pattern -> +          if String.contains?(pattern, "*") do +            ~r(#{String.replace(pattern, "*", ".*")}) +          else +            pattern +          end +        end) + +      Enum.any?(patterns, matcher) && group      end)    end  end diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex index 4259d5718..58ab3650d 100644 --- a/lib/pleroma/flake_id.ex +++ b/lib/pleroma/flake_id.ex @@ -46,7 +46,7 @@ defmodule Pleroma.FlakeId do    def from_string(string) when is_binary(string) and byte_size(string) < 18 do      case Integer.parse(string) do -      {id, _} -> <<0::integer-size(64), id::integer-size(64)>> +      {id, ""} -> <<0::integer-size(64), id::integer-size(64)>>        _ -> nil      end    end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index e3625383b..8ea9dbd38 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -77,9 +77,9 @@ defmodule Pleroma.Formatter do    def emojify(text, nil), do: text    def emojify(text, emoji, strip \\ false) do -    Enum.reduce(emoji, text, fn {emoji, file}, text -> -      emoji = HTML.strip_tags(emoji) -      file = HTML.strip_tags(file) +    Enum.reduce(emoji, text, fn emoji_data, text -> +      emoji = HTML.strip_tags(elem(emoji_data, 0)) +      file = HTML.strip_tags(elem(emoji_data, 1))        html =          if not strip do @@ -101,7 +101,7 @@ defmodule Pleroma.Formatter do    def demojify(text, nil), do: text    def get_emoji(text) when is_binary(text) do -    Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end) +    Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)    end    def get_emoji(_), do: [] diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 3b9629d77..6a56a6f67 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -38,7 +38,6 @@ end  defmodule Pleroma.Gopher.Server.ProtocolHandler do    alias Pleroma.Activity    alias Pleroma.HTML -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Visibility @@ -111,7 +110,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do    end    def response("/notices/" <> id) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.is_public?(activity) do        activities =          ActivityPub.fetch_activities_for_context(activity.data["context"]) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 5b152d926..7f1dbe28c 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -28,27 +28,39 @@ defmodule Pleroma.HTML do    def filter_tags(html), do: filter_tags(html, nil)    def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) -  def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do -    key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}" -    Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end) +  def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do +    key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" + +    Cachex.fetch!(:scrubber_cache, key, fn _key -> +      ensure_scrubbed_html(content, scrubbers, activity.data["object"]["fake"] || false) +    end)    end -  def get_cached_stripped_html_for_object(content, object, module) do -    get_cached_scrubbed_html_for_object( +  def get_cached_stripped_html_for_activity(content, activity, key) do +    get_cached_scrubbed_html_for_activity(        content,        HtmlSanitizeEx.Scrubber.StripTags, -      object, -      module +      activity, +      key      )    end    def ensure_scrubbed_html(          content, -        scrubbers +        scrubbers, +        false = _fake        ) do      {:commit, filter_tags(content, scrubbers)}    end +  def ensure_scrubbed_html( +        content, +        scrubbers, +        true = _fake +      ) do +    {:ignore, filter_tags(content, scrubbers)} +  end +    defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do      generate_scrubber_signature([scrubber])    end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index 55c4cf6df..110be8355 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -80,7 +80,7 @@ defmodule Pleroma.List do    # Get lists to which the account belongs.    def get_lists_account_belongs(%User{} = owner, account_id) do -    user = Repo.get(User, account_id) +    user = User.get_by_id(account_id)      query =        from( diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 8a670645d..013d62157 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -44,6 +44,11 @@ defmodule Pleroma.Object do    # Use this whenever possible, especially when walking graphs in an O(N) loop!    def normalize(%Activity{object: %Object{} = object}), do: object +  # A hack for fake activities +  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}) do +    %Object{id: "pleroma:fake_object_id", data: data} +  end +    # Catch and log Object.normalize() calls where the Activity's child object is not    # preloaded.    def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex index 5a77f6833..4089aa958 100644 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ b/lib/pleroma/plugs/user_fetcher_plug.ex @@ -3,9 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Plugs.UserFetcherPlug do -  alias Pleroma.Repo    alias Pleroma.User -    import Plug.Conn    def init(options) do @@ -14,26 +12,10 @@ defmodule Pleroma.Plugs.UserFetcherPlug do    def call(conn, _options) do      with %{auth_credentials: %{username: username}} <- conn.assigns, -         {:ok, %User{} = user} <- user_fetcher(username) do -      conn -      |> assign(:auth_user, user) +         %User{} = user <- User.get_by_nickname_or_email(username) do +      assign(conn, :auth_user, user)      else        _ -> conn      end    end - -  defp user_fetcher(username_or_email) do -    { -      :ok, -      cond do -        # First, try logging in as if it was a name -        user = Repo.get_by(User, %{nickname: username_or_email}) -> -          user - -        # If we get nil, we try using it as an email -        user = Repo.get_by(User, %{email: username_or_email}) -> -          user -      end -    } -  end  end diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex new file mode 100644 index 000000000..21fd1fc3f --- /dev/null +++ b/lib/pleroma/registration.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Registration do +  use Ecto.Schema + +  import Ecto.Changeset + +  alias Pleroma.Registration +  alias Pleroma.Repo +  alias Pleroma.User + +  @primary_key {:id, Pleroma.FlakeId, autogenerate: true} + +  schema "registrations" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:provider, :string) +    field(:uid, :string) +    field(:info, :map, default: %{}) + +    timestamps() +  end + +  def nickname(registration, default \\ nil), +    do: Map.get(registration.info, "nickname", default) + +  def email(registration, default \\ nil), +    do: Map.get(registration.info, "email", default) + +  def name(registration, default \\ nil), +    do: Map.get(registration.info, "name", default) + +  def description(registration, default \\ nil), +    do: Map.get(registration.info, "description", default) + +  def changeset(registration, params \\ %{}) do +    registration +    |> cast(params, [:user_id, :provider, :uid, :info]) +    |> validate_required([:provider, :uid]) +    |> foreign_key_constraint(:user_id) +    |> unique_constraint(:uid, name: :registrations_provider_uid_index) +  end + +  def bind_to_user(registration, user) do +    registration +    |> changeset(%{user_id: (user && user.id) || nil}) +    |> Repo.update() +  end + +  def get_by_provider_uid(provider, uid) do +    Repo.get_by(Registration, +      provider: to_string(provider), +      uid: to_string(uid) +    ) +  end +end diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index 4af1bde56..aa5d427ae 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,6 +8,10 @@ defmodule Pleroma.Repo do      adapter: Ecto.Adapters.Postgres,      migration_timestamps: [type: :naive_datetime_usec] +  defmodule Instrumenter do +    use Prometheus.EctoInstrumenter +  end +    @doc """    Dynamically loads the repository url from the    DATABASE_URL environment variable. diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex new file mode 100644 index 000000000..de0e54699 --- /dev/null +++ b/lib/pleroma/scheduled_activity.ex @@ -0,0 +1,161 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivity do +  use Ecto.Schema + +  alias Pleroma.Config +  alias Pleroma.Repo +  alias Pleroma.ScheduledActivity +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI.Utils + +  import Ecto.Query +  import Ecto.Changeset + +  @min_offset :timer.minutes(5) + +  schema "scheduled_activities" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:scheduled_at, :naive_datetime) +    field(:params, :map) + +    timestamps() +  end + +  def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> cast(attrs, [:scheduled_at, :params]) +    |> validate_required([:scheduled_at, :params]) +    |> validate_scheduled_at() +    |> with_media_attachments() +  end + +  defp with_media_attachments( +         %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset +       ) +       when is_list(media_ids) do +    media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids}) + +    params = +      params +      |> Map.put("media_attachments", media_attachments) +      |> Map.put("media_ids", media_ids) + +    put_change(changeset, :params, params) +  end + +  defp with_media_attachments(changeset), do: changeset + +  def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> cast(attrs, [:scheduled_at]) +    |> validate_required([:scheduled_at]) +    |> validate_scheduled_at() +  end + +  def validate_scheduled_at(changeset) do +    validate_change(changeset, :scheduled_at, fn _, scheduled_at -> +      cond do +        not far_enough?(scheduled_at) -> +          [scheduled_at: "must be at least 5 minutes from now"] + +        exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) -> +          [scheduled_at: "daily limit exceeded"] + +        exceeds_total_user_limit?(changeset.data.user_id) -> +          [scheduled_at: "total limit exceeded"] + +        true -> +          [] +      end +    end) +  end + +  def exceeds_daily_user_limit?(user_id, scheduled_at) do +    ScheduledActivity +    |> where(user_id: ^user_id) +    |> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date)) +    |> select([sa], count(sa.id)) +    |> Repo.one() +    |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit])) +  end + +  def exceeds_total_user_limit?(user_id) do +    ScheduledActivity +    |> where(user_id: ^user_id) +    |> select([sa], count(sa.id)) +    |> Repo.one() +    |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit])) +  end + +  def far_enough?(scheduled_at) when is_binary(scheduled_at) do +    with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do +      far_enough?(scheduled_at) +    else +      _ -> false +    end +  end + +  def far_enough?(scheduled_at) do +    now = NaiveDateTime.utc_now() +    diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) +    diff > @min_offset +  end + +  def new(%User{} = user, attrs) do +    %ScheduledActivity{user_id: user.id} +    |> changeset(attrs) +  end + +  def create(%User{} = user, attrs) do +    user +    |> new(attrs) +    |> Repo.insert() +  end + +  def get(%User{} = user, scheduled_activity_id) do +    ScheduledActivity +    |> where(user_id: ^user.id) +    |> where(id: ^scheduled_activity_id) +    |> Repo.one() +  end + +  def update(%ScheduledActivity{} = scheduled_activity, attrs) do +    scheduled_activity +    |> update_changeset(attrs) +    |> Repo.update() +  end + +  def delete(%ScheduledActivity{} = scheduled_activity) do +    scheduled_activity +    |> Repo.delete() +  end + +  def delete(id) when is_binary(id) or is_integer(id) do +    ScheduledActivity +    |> where(id: ^id) +    |> select([sa], sa) +    |> Repo.delete_all() +    |> case do +      {1, [scheduled_activity]} -> {:ok, scheduled_activity} +      _ -> :error +    end +  end + +  def for_user_query(%User{} = user) do +    ScheduledActivity +    |> where(user_id: ^user.id) +  end + +  def due_activities(offset \\ 0) do +    naive_datetime = +      NaiveDateTime.utc_now() +      |> NaiveDateTime.add(offset, :millisecond) + +    ScheduledActivity +    |> where([sa], sa.scheduled_at < ^naive_datetime) +    |> Repo.all() +  end +end diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex new file mode 100644 index 000000000..65b38622f --- /dev/null +++ b/lib/pleroma/scheduled_activity_worker.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityWorker do +  @moduledoc """ +  Sends scheduled activities to the job queue. +  """ + +  alias Pleroma.Config +  alias Pleroma.ScheduledActivity +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI +  use GenServer +  require Logger + +  @schedule_interval :timer.minutes(1) + +  def start_link do +    GenServer.start_link(__MODULE__, nil) +  end + +  def init(_) do +    if Config.get([ScheduledActivity, :enabled]) do +      schedule_next() +      {:ok, nil} +    else +      :ignore +    end +  end + +  def perform(:execute, scheduled_activity_id) do +    try do +      {:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id) +      %User{} = user = User.get_cached_by_id(scheduled_activity.user_id) +      {:ok, _result} = CommonAPI.post(user, scheduled_activity.params) +    rescue +      error -> +        Logger.error( +          "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}" +        ) +    end +  end + +  def handle_info(:perform, state) do +    ScheduledActivity.due_activities(@schedule_interval) +    |> Enum.each(fn scheduled_activity -> +      PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id]) +    end) + +    schedule_next() +    {:noreply, state} +  end + +  defp schedule_next do +    Process.send_after(self(), :perform, @schedule_interval) +  end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 728b00a56..05f56c01e 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -13,6 +13,7 @@ defmodule Pleroma.User do    alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web @@ -55,6 +56,7 @@ defmodule Pleroma.User do      field(:bookmarks, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime_usec)      has_many(:notifications, Notification) +    has_many(:registrations, Registration)      embeds_one(:info, Pleroma.User.Info)      timestamps() @@ -216,7 +218,7 @@ defmodule Pleroma.User do      changeset =        struct        |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) -      |> validate_required([:email, :name, :nickname, :password, :password_confirmation]) +      |> validate_required([:name, :nickname, :password, :password_confirmation])        |> validate_confirmation(:password)        |> unique_constraint(:email)        |> unique_constraint(:nickname) @@ -227,6 +229,13 @@ defmodule Pleroma.User do        |> validate_length(:name, min: 1, max: 100)        |> put_change(:info, info_change) +    changeset = +      if opts[:external] do +        changeset +      else +        validate_required(changeset, [:email]) +      end +      if changeset.valid? do        hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) @@ -505,11 +514,10 @@ defmodule Pleroma.User do        end    end +  def get_by_email(email), do: Repo.get_by(User, email: email) +    def get_by_nickname_or_email(nickname_or_email) do -    case user = Repo.get_by(User, nickname: nickname_or_email) do -      %User{} -> user -      nil -> Repo.get_by(User, email: nickname_or_email) -    end +    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)    end    def get_cached_user_info(user) do @@ -1088,28 +1096,27 @@ defmodule Pleroma.User do      # Remove all relationships      {:ok, followers} = User.get_followers(user) -    followers -    |> Enum.each(fn follower -> User.unfollow(follower, user) end) +    Enum.each(followers, fn follower -> User.unfollow(follower, user) end)      {:ok, friends} = User.get_friends(user) -    friends -    |> Enum.each(fn followed -> User.unfollow(user, followed) end) +    Enum.each(friends, fn followed -> User.unfollow(user, followed) end) -    query = -      from(a in Activity, where: a.actor == ^user.ap_id) -      |> Activity.with_preloaded_object() +    delete_user_activities(user) +  end -    Repo.all(query) -    |> Enum.each(fn activity -> -      case activity.data["type"] do -        "Create" -> -          ActivityPub.delete(Object.normalize(activity)) +  def delete_user_activities(%User{ap_id: ap_id} = user) do +    Activity +    |> where(actor: ^ap_id) +    |> Activity.with_preloaded_object() +    |> Repo.all() +    |> Enum.each(fn +      %{data: %{"type" => "Create"}} = activity -> +        activity |> Object.normalize() |> ActivityPub.delete() -        # TODO: Do something with likes, follows, repeats. -        _ -> -          "Doing nothing" -      end +      # TODO: Do something with likes, follows, repeats. +      _ -> +        "Doing nothing"      end)      {:ok, user} @@ -1231,8 +1238,8 @@ defmodule Pleroma.User do    # this is because we have synchronous follow APIs and need to simulate them    # with an async handshake    def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do -    with %User{} = a <- Repo.get(User, a.id), -         %User{} = b <- Repo.get(User, b.id) do +    with %User{} = a <- User.get_by_id(a.id), +         %User{} = b <- User.get_by_id(b.id) do        {:ok, a, b}      else        _e -> @@ -1242,8 +1249,8 @@ defmodule Pleroma.User do    def wait_and_refresh(timeout, %User{} = a, %User{} = b) do      with :ok <- :timer.sleep(timeout), -         %User{} = a <- Repo.get(User, a.id), -         %User{} = b <- Repo.get(User, b.id) do +         %User{} = a <- User.get_by_id(a.id), +         %User{} = b <- User.get_by_id(b.id) do        {:ok, a, b}      else        _e -> diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 6e1ed7ec9..f217e7bac 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -113,15 +113,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def decrease_replies_count_if_reply(_object), do: :noop -  def insert(map, local \\ true) when is_map(map) do +  def insert(map, local \\ true, fake \\ false) when is_map(map) do      with nil <- Activity.normalize(map), -         map <- lazy_put_activity_defaults(map), +         map <- lazy_put_activity_defaults(map, fake),           :ok <- check_actor_is_active(map["actor"]),           {_, true} <- {:remote_limit_error, check_remote_limit(map)},           {:ok, map} <- MRF.filter(map), +         {recipients, _, _} = get_recipients(map), +         {:fake, false, map, recipients} <- {:fake, fake, map, recipients},           {:ok, object} <- insert_full_object(map) do -      {recipients, _, _} = get_recipients(map) -        {:ok, activity} =          Repo.insert(%Activity{            data: map, @@ -146,8 +146,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        stream_out(activity)        {:ok, activity}      else -      %Activity{} = activity -> {:ok, activity} -      error -> {:error, error} +      %Activity{} = activity -> +        {:ok, activity} + +      {:fake, true, map, recipients} -> +        activity = %Activity{ +          data: map, +          local: local, +          actor: map["actor"], +          recipients: recipients, +          id: "pleroma:fakeid" +        } + +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +        {:ok, activity} + +      error -> +        {:error, error}      end    end @@ -190,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def create(%{to: to, actor: actor, context: context, object: object} = params) do +  def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do      additional = params[:additional] || %{}      # only accept false as false value      local = !(params[:local] == false) @@ -201,13 +216,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do               %{to: to, actor: actor, published: published, context: context, object: object},               additional             ), -         {:ok, activity} <- insert(create_data, local), +         {:ok, activity} <- insert(create_data, local, fake), +         {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           :ok <- maybe_federate(activity) do        {:ok, activity} +    else +      {:fake, true, activity} -> +        {:ok, activity}      end    end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f733ae7e1..593ae3188 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -954,7 +954,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    defp strip_internal_tags(object), do: object -  defp user_upgrade_task(user) do +  def perform(:user_upgrade, user) do      # we pass a fake user so that the followers collection is stripped away      old_follower_address = User.ap_followers(%User{nickname: user.nickname}) @@ -999,28 +999,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      Repo.update_all(q, [])    end -  def upgrade_user_from_ap_id(ap_id, async \\ true) do +  def upgrade_user_from_ap_id(ap_id) do      with %User{local: false} = user <- User.get_by_ap_id(ap_id), -         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do -      already_ap = User.ap_enabled?(user) - -      {:ok, user} = -        User.upgrade_changeset(user, data) -        |> Repo.update() - -      if !already_ap do -        # This could potentially take a long time, do it in the background -        if async do -          Task.start(fn -> -            user_upgrade_task(user) -          end) -        else -          user_upgrade_task(user) -        end +         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), +         already_ap <- User.ap_enabled?(user), +         {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do +      unless already_ap do +        PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])        end        {:ok, user}      else +      %User{} = user -> {:ok, user}        e -> e      end    end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 2e9ffe41c..0b53f71c3 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -99,7 +99,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do      %{        "@context" => [          "https://www.w3.org/ns/activitystreams", -        "#{Web.base_url()}/schemas/litepub-0.1.jsonld" +        "#{Web.base_url()}/schemas/litepub-0.1.jsonld", +        %{ +          "@language" => "und" +        }        ]      }    end @@ -175,18 +178,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do    Adds an id and a published data if they aren't there,    also adds it to an included object    """ -  def lazy_put_activity_defaults(map) do -    %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) - +  def lazy_put_activity_defaults(map, fake \\ false) do      map = -      map -      |> Map.put_new_lazy("id", &generate_activity_id/0) -      |> Map.put_new_lazy("published", &make_date/0) -      |> Map.put_new("context", context) -      |> Map.put_new("context_id", context_id) +      unless fake do +        %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) + +        map +        |> Map.put_new_lazy("id", &generate_activity_id/0) +        |> Map.put_new_lazy("published", &make_date/0) +        |> Map.put_new("context", context) +        |> Map.put_new("context_id", context_id) +      else +        map +        |> Map.put_new("id", "pleroma:fakeid") +        |> Map.put_new_lazy("published", &make_date/0) +        |> Map.put_new("context", "pleroma:fakecontext") +        |> Map.put_new("context_id", -1) +      end      if is_map(map["object"]) do -      object = lazy_put_object_defaults(map["object"], map) +      object = lazy_put_object_defaults(map["object"], map, fake)        %{map | "object" => object}      else        map @@ -196,7 +207,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @doc """    Adds an id and published date if they aren't there.    """ -  def lazy_put_object_defaults(map, activity \\ %{}) do +  def lazy_put_object_defaults(map, activity \\ %{}, fake) + +  def lazy_put_object_defaults(map, activity, true = _fake) do +    map +    |> Map.put_new_lazy("published", &make_date/0) +    |> Map.put_new("id", "pleroma:fake_object_id") +    |> Map.put_new("context", activity["context"]) +    |> Map.put_new("fake", true) +    |> Map.put_new("context_id", activity["context_id"]) +  end + +  def lazy_put_object_defaults(map, activity, _fake) do      map      |> Map.put_new_lazy("id", &generate_object_id/0)      |> Map.put_new_lazy("published", &make_date/0) @@ -354,7 +376,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do          [state, actor, object]        ) -      activity = Repo.get(Activity, activity.id) +      activity = Activity.get_by_id(activity.id)        {:ok, activity}      rescue        e -> @@ -404,13 +426,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do              activity.data            ),          where: activity.actor == ^follower_id, +        # this is to use the index          where:            fragment( -            "? @> ?", +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?",              activity.data, -            ^%{object: followed_id} +            activity.data, +            ^followed_id            ), -        order_by: [desc: :id], +        order_by: [fragment("? desc nulls last", activity.id)],          limit: 1        ) @@ -567,13 +591,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do              activity.data            ),          where: activity.actor == ^blocker_id, +        # this is to use the index          where:            fragment( -            "? @> ?", +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", +            activity.data,              activity.data, -            ^%{object: blocked_id} +            ^blocked_id            ), -        order_by: [desc: :id], +        order_by: [fragment("? desc nulls last", activity.id)],          limit: 1        ) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index b3a09e49e..78bf31893 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -25,6 +25,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> json(nickname)    end +  def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do +    with %User{} = follower <- User.get_by_nickname(follower_nick), +         %User{} = followed <- User.get_by_nickname(followed_nick) do +      User.follow(follower, followed) +    end + +    conn +    |> json("ok") +  end + +  def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do +    with %User{} = follower <- User.get_by_nickname(follower_nick), +         %User{} = followed <- User.get_by_nickname(followed_nick) do +      User.unfollow(follower, followed) +    end + +    conn +    |> json("ok") +  end +    def user_create(          conn,          %{"nickname" => nickname, "email" => email, "password" => password} diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 82267c595..89d88af32 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Auth.Authenticator do +  alias Pleroma.Registration    alias Pleroma.User    def implementation do @@ -12,14 +13,33 @@ defmodule Pleroma.Web.Auth.Authenticator do      )    end -  @callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()} -  def get_user(plug), do: implementation().get_user(plug) +  @callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()} +  def get_user(plug, params), do: implementation().get_user(plug, params) + +  @callback create_from_registration(Plug.Conn.t(), Map.t(), Registration.t()) :: +              {:ok, User.t()} | {:error, any()} +  def create_from_registration(plug, params, registration), +    do: implementation().create_from_registration(plug, params, registration) + +  @callback get_registration(Plug.Conn.t(), Map.t()) :: +              {:ok, Registration.t()} | {:error, any()} +  def get_registration(plug, params), +    do: implementation().get_registration(plug, params)    @callback handle_error(Plug.Conn.t(), any()) :: any()    def handle_error(plug, error), do: implementation().handle_error(plug, error)    @callback auth_template() :: String.t() | nil    def auth_template do -    implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html") +    # Note: `config :pleroma, :auth_template, "..."` support is deprecated +    implementation().auth_template() || +      Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) || +      "show.html" +  end + +  @callback oauth_consumer_template() :: String.t() | nil +  def oauth_consumer_template do +    implementation().oauth_consumer_template() || +      Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")    end  end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 88217aab8..8b6d5a77f 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -8,14 +8,19 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    require Logger    @behaviour Pleroma.Web.Auth.Authenticator +  @base Pleroma.Web.Auth.PleromaAuthenticator    @connection_timeout 10_000    @search_timeout 10_000 -  def get_user(%Plug.Conn{} = conn) do +  defdelegate get_registration(conn, params), to: @base + +  defdelegate create_from_registration(conn, params, registration), to: @base + +  def get_user(%Plug.Conn{} = conn, params) do      if Pleroma.Config.get([:ldap, :enabled]) do        {name, password} = -        case conn.params do +        case params do            %{"authorization" => %{"name" => name, "password" => password}} ->              {name, password} @@ -29,14 +34,14 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do          {:error, {:ldap_connection_error, _}} ->            # When LDAP is unavailable, try default authenticator -          Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +          @base.get_user(conn, params)          error ->            error        end      else        # Fall back to default authenticator -      Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +      @base.get_user(conn, params)      end    end @@ -46,6 +51,8 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    def auth_template, do: nil +  def oauth_consumer_template, do: nil +    defp ldap_user(name, password) do      ldap = Pleroma.Config.get(:ldap, [])      host = Keyword.get(ldap, :host, "localhost") diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 94a19ad49..c826adb4c 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -4,13 +4,15 @@  defmodule Pleroma.Web.Auth.PleromaAuthenticator do    alias Comeonin.Pbkdf2 +  alias Pleroma.Registration +  alias Pleroma.Repo    alias Pleroma.User    @behaviour Pleroma.Web.Auth.Authenticator -  def get_user(%Plug.Conn{} = conn) do +  def get_user(%Plug.Conn{} = _conn, params) do      {name, password} = -      case conn.params do +      case params do          %{"authorization" => %{"name" => name, "password" => password}} ->            {name, password} @@ -27,9 +29,69 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do      end    end +  def get_registration( +        %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}}, +        _params +      ) do +    registration = Registration.get_by_provider_uid(provider, uid) + +    if registration do +      {:ok, registration} +    else +      info = auth.info + +      Registration.changeset(%Registration{}, %{ +        provider: to_string(provider), +        uid: to_string(uid), +        info: %{ +          "nickname" => info.nickname, +          "email" => info.email, +          "name" => info.name, +          "description" => info.description +        } +      }) +      |> Repo.insert() +    end +  end + +  def get_registration(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials} + +  def create_from_registration(_conn, params, registration) do +    nickname = value([params["nickname"], Registration.nickname(registration)]) +    email = value([params["email"], Registration.email(registration)]) +    name = value([params["name"], Registration.name(registration)]) || nickname +    bio = value([params["bio"], Registration.description(registration)]) + +    random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() + +    with {:ok, new_user} <- +           User.register_changeset( +             %User{}, +             %{ +               email: email, +               nickname: nickname, +               name: name, +               bio: bio, +               password: random_password, +               password_confirmation: random_password +             }, +             external: true, +             confirmed: true +           ) +           |> Repo.insert(), +         {:ok, _} <- +           Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do +      {:ok, new_user} +    end +  end + +  defp value(list), do: Enum.find(list, &(to_string(&1) != "")) +    def handle_error(%Plug.Conn{} = _conn, error) do      error    end    def auth_template, do: nil + +  def oauth_consumer_template, do: nil  end diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex index 3a700fa3b..6503979a1 100644 --- a/lib/pleroma/web/channels/user_socket.ex +++ b/lib/pleroma/web/channels/user_socket.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do    def connect(%{"token" => token}, socket) do      with true <- Pleroma.Config.get([:chat, :enabled]),           {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600), -         %User{} = user <- Pleroma.Repo.get(User, user_id) do +         %User{} = user <- Pleroma.User.get_by_id(user_id) do        {:ok, assign(socket, :user_name, user.nickname)}      else        _e -> :error diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 25b990677..74babdf14 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -167,18 +167,21 @@ defmodule Pleroma.Web.CommonAPI do               object,               "emoji",               (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"])) -             |> Enum.reduce(%{}, fn {name, file}, acc -> +             |> Enum.reduce(%{}, fn {name, file, _}, acc ->                 Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")               end)             ) do        res = -        ActivityPub.create(%{ -          to: to, -          actor: user, -          context: context, -          object: object, -          additional: %{"cc" => cc, "directMessage" => visibility == "direct"} -        }) +        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      end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index f596f703b..051db6c79 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -15,6 +15,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do    alias Pleroma.Web.Endpoint    alias Pleroma.Web.MediaProxy +  require Logger +    # This is a hack for twidere.    def get_by_id_or_ap_id(id) do      activity = @@ -31,7 +33,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def get_replied_to_activity(""), do: nil    def get_replied_to_activity(id) when not is_nil(id) do -    Repo.get(Activity, id) +    Activity.get_by_id(id)    end    def get_replied_to_activity(_), do: nil @@ -240,15 +242,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do      Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")    end -  def date_to_asctime(date) do -    with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do +  def date_to_asctime(date) when is_binary(date) do +    with {:ok, date, _offset} <- DateTime.from_iso8601(date) do        format_asctime(date)      else        _e -> +        Logger.warn("Date #{date} in wrong format, must be ISO 8601")          ""      end    end +  def date_to_asctime(date) do +    Logger.warn("Date #{date} in wrong format, must be ISO 8601") +    "" +  end +    def to_masto_date(%NaiveDateTime{} = date) do      date      |> NaiveDateTime.to_iso8601() @@ -275,7 +283,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    def confirm_current_password(user, password) do -    with %User{local: true} = db_user <- Repo.get(User, user.id), +    with %User{local: true} = db_user <- User.get_by_id(user.id),           true <- Pbkdf2.checkpw(password, db_user.password_hash) do        {:ok, db_user}      else @@ -285,7 +293,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def emoji_from_profile(%{info: _info} = user) do      (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name)) -    |> Enum.map(fn {shortcode, url} -> +    |> Enum.map(fn {shortcode, url, _} ->        %{          "type" => "Emoji",          "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"}, diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 4d6192db0..181483664 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,6 +5,11 @@  defmodule Pleroma.Web.ControllerHelper do    use Pleroma.Web, :controller +  # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html +  @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"] +  def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil +  def truthy_param?(value), do: value not in @falsy_param_values +    def oauth_scopes(params, default) do      # Note: `scopes` is used by Mastodon — supporting it but sticking to      # OAuth's standard `scope` wherever we control it diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index fa2d1cbe7..1633477c3 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -51,11 +51,22 @@ defmodule Pleroma.Web.Endpoint do    plug(Plug.MethodOverride)    plug(Plug.Head) +  secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag]) +    cookie_name = -    if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), +    if secure_cookies,        do: "__Host-pleroma_key",        else: "pleroma_key" +  same_site = +    if Pleroma.Config.oauth_consumer_enabled?() do +      # Note: "SameSite=Strict" prevents sign in with external OAuth provider +      #   (there would be no cookies during callback request from OAuth provider) +      "SameSite=Lax" +    else +      "SameSite=Strict" +    end +    # The session will be stored in the cookie and signed,    # this means its contents can be read but not tampered with.    # Set :encryption_salt if you would also like to encrypt it. @@ -65,11 +76,30 @@ defmodule Pleroma.Web.Endpoint do      key: cookie_name,      signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},      http_only: true, -    secure: -      Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), -    extra: "SameSite=Strict" +    secure: secure_cookies, +    extra: same_site    ) +  # Note: the plug and its configuration is compile-time this can't be upstreamed yet +  if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do +    plug(RemoteIp, proxies: proxies) +  end + +  defmodule Instrumenter do +    use Prometheus.PhoenixInstrumenter +  end + +  defmodule PipelineInstrumenter do +    use Prometheus.PlugPipelineInstrumenter +  end + +  defmodule MetricsExporter do +    use Prometheus.PlugExporter +  end + +  plug(PipelineInstrumenter) +  plug(MetricsExporter) +    plug(Pleroma.Web.Router)    @doc """ diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 08ea5f967..382f07e6b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Pagination +  alias Pleroma.ScheduledActivity    alias Pleroma.User    def get_followers(user, params \\ %{}) do @@ -28,6 +29,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      |> Pagination.fetch_paginated(params)    end +  def get_scheduled_activities(user, params \\ %{}) do +    user +    |> ScheduledActivity.for_user_query() +    |> Pagination.fetch_paginated(params) +  end +    defp cast_params(params) do      param_types = %{        exclude_types: {:array, :string} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index eee4e7678..5462ce8be 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -5,12 +5,14 @@  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller +  alias Ecto.Changeset    alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Filter    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo +  alias Pleroma.ScheduledActivity    alias Pleroma.Stats    alias Pleroma.User    alias Pleroma.Web @@ -25,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web.MastodonAPI.MastodonView    alias Pleroma.Web.MastodonAPI.NotificationView    alias Pleroma.Web.MastodonAPI.ReportView +  alias Pleroma.Web.MastodonAPI.ScheduledActivityView    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.MediaProxy    alias Pleroma.Web.OAuth.App @@ -178,14 +181,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    defp mastodonized_emoji do      Pleroma.Emoji.get_all() -    |> Enum.map(fn {shortcode, relative_url} -> +    |> Enum.map(fn {shortcode, relative_url, tags} ->        url = to_string(URI.merge(Web.base_url(), relative_url))        %{          "shortcode" => shortcode,          "static_url" => url,          "visible_in_picker" => true, -        "url" => url +        "url" => url, +        "tags" => String.split(tags, ",")        }      end)    end @@ -285,7 +289,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do -    with %User{} = user <- Repo.get(User, params["id"]) do +    with %User{} = user <- User.get_by_id(params["id"]) do        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -319,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.visible_for_user?(activity, user) do        conn        |> put_view(StatusView) @@ -328,7 +332,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           activities <-             ActivityPub.fetch_activities_for_context(activity.data["context"], %{               "blocking_user" => user, @@ -364,6 +368,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do +    with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do +      conn +      |> add_link_headers(:scheduled_statuses, scheduled_activities) +      |> put_view(ScheduledActivityView) +      |> render("index.json", %{scheduled_activities: scheduled_activities}) +    end +  end + +  def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      _ -> {:error, :not_found} +    end +  end + +  def update_scheduled_status( +        %{assigns: %{user: user}} = conn, +        %{"id" => scheduled_activity_id} = params +      ) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id), +         {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      nil -> {:error, :not_found} +      error -> error +    end +  end + +  def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do +    with %ScheduledActivity{} = scheduled_activity <- +           ScheduledActivity.get(user, scheduled_activity_id), +         {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do +      conn +      |> put_view(ScheduledActivityView) +      |> render("show.json", %{scheduled_activity: scheduled_activity}) +    else +      nil -> {:error, :not_found} +      error -> error +    end +  end +    def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)        when length(media_ids) > 0 do      params = @@ -384,12 +437,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          _ -> Ecto.UUID.generate()        end -    {:ok, activity} = -      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end) +    scheduled_at = params["scheduled_at"] -    conn -    |> put_view(StatusView) -    |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do +      with {:ok, scheduled_activity} <- +             ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do +        conn +        |> put_view(ScheduledActivityView) +        |> render("show.json", %{scheduled_activity: scheduled_activity}) +      end +    else +      params = Map.drop(params, ["scheduled_at"]) + +      {:ok, activity} = +        Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> +          CommonAPI.post(user, params) +        end) + +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end    end    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do @@ -460,7 +528,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           %User{} = user <- User.get_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do @@ -471,7 +539,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           %User{} = user <- User.get_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do @@ -593,7 +661,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def favourited_by(conn, %{"id" => id}) do -    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do +    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do        q = from(u in User, where: u.ap_id in ^likes)        users = Repo.all(q) @@ -606,7 +674,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reblogged_by(conn, %{"id" => id}) do -    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do +    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do        q = from(u in User, where: u.ap_id in ^announces)        users = Repo.all(q) @@ -657,7 +725,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- Repo.get(User, id), +    with %User{} = user <- User.get_by_id(id),           followers <- MastodonAPI.get_followers(user, params) do        followers =          cond do @@ -674,7 +742,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- Repo.get(User, id), +    with %User{} = user <- User.get_by_id(id),           followers <- MastodonAPI.get_friends(user, params) do        followers =          cond do @@ -699,7 +767,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- Repo.get(User, id), +    with %User{} = follower <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -713,7 +781,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- Repo.get(User, id), +    with %User{} = follower <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -727,7 +795,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with %User{} = followed <- Repo.get(User, id), +    with %User{} = followed <- User.get_by_id(id),           false <- User.following?(follower, followed),           {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn @@ -755,7 +823,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do -    with %User{} = followed <- Repo.get_by(User, nickname: uri), +    with %User{} = followed <- User.get_by_nickname(uri),           {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn        |> put_view(AccountView) @@ -769,7 +837,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with %User{} = followed <- Repo.get(User, id), +    with %User{} = followed <- User.get_by_id(id),           {:ok, follower} <- CommonAPI.unfollow(follower, followed) do        conn        |> put_view(AccountView) @@ -778,7 +846,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- Repo.get(User, id), +    with %User{} = muted <- User.get_by_id(id),           {:ok, muter} <- User.mute(muter, muted) do        conn        |> put_view(AccountView) @@ -792,7 +860,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- Repo.get(User, id), +    with %User{} = muted <- User.get_by_id(id),           {:ok, muter} <- User.unmute(muter, muted) do        conn        |> put_view(AccountView) @@ -813,7 +881,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- Repo.get(User, id), +    with %User{} = blocked <- User.get_by_id(id),           {:ok, blocker} <- User.block(blocker, blocked),           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do        conn @@ -828,7 +896,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- Repo.get(User, id), +    with %User{} = blocked <- User.get_by_id(id),           {:ok, blocker} <- User.unblock(blocker, blocked),           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do        conn @@ -966,7 +1034,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def bookmarks(%{assigns: %{user: user}} = conn, _) do -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      activities =        user.bookmarks @@ -1023,7 +1091,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- Repo.get(User, account_id) do +           %User{} = followed <- User.get_by_id(account_id) do          Pleroma.List.follow(list, followed)        end      end) @@ -1035,7 +1103,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- Repo.get(Pleroma.User, account_id) do +           %User{} = followed <- Pleroma.User.get_by_id(account_id) do          Pleroma.List.unfollow(list, followed)        end      end) @@ -1091,9 +1159,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def index(%{assigns: %{user: user}} = conn, _params) do -    token = -      conn -      |> get_session(:oauth_token) +    token = get_session(conn, :oauth_token)      if user && token do        mastodon_emoji = mastodonized_emoji() @@ -1121,7 +1187,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              auto_play_gif: false,              display_sensitive_media: false,              reduce_motion: false, -            max_toot_chars: limit +            max_toot_chars: limit, +            mascot: "/images/pleroma-fox-tan-smol.png"            },            rights: %{              delete_others_notice: present?(user.info.is_moderator), @@ -1193,6 +1260,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> render("index.html", %{initial_state: initial_state, flavour: flavour})      else        conn +      |> put_session(:return_to, conn.request_path)        |> redirect(to: "/web/login")      end    end @@ -1249,16 +1317,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      "glitch"    end -  def login(conn, %{"code" => code}) do +  def login(%{assigns: %{user: %User{}}} = conn, _params) do +    redirect(conn, to: local_mastodon_root_path(conn)) +  end + +  @doc "Local Mastodon FE login init action" +  def login(conn, %{"code" => auth_token}) do      with {:ok, app} <- get_or_make_app(), -         %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id), +         %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),           {:ok, token} <- Token.exchange_token(app, auth) do        conn        |> put_session(:oauth_token, token.token) -      |> redirect(to: "/web/getting-started") +      |> redirect(to: local_mastodon_root_path(conn))      end    end +  @doc "Local Mastodon FE callback action"    def login(conn, _) do      with {:ok, app} <- get_or_make_app() do        path = @@ -1271,8 +1345,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            scope: Enum.join(app.scopes, " ")          ) -      conn -      |> redirect(to: path) +      redirect(conn, to: path) +    end +  end + +  defp local_mastodon_root_path(conn) do +    case get_session(conn, :return_to) do +      nil -> +        mastodon_api_path(conn, :index, ["getting-started"]) + +      return_to -> +        delete_session(conn, :return_to) +        return_to      end    end @@ -1312,7 +1396,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do      Logger.debug("Unimplemented, returning unmodified relationship") -    with %User{} = target <- Repo.get(User, id) do +    with %User{} = target <- User.get_by_id(id) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: user, target: target}) @@ -1390,6 +1474,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    # fallback action    # +  def errors(conn, {:error, %Changeset{} = changeset}) do +    error_message = +      changeset +      |> Changeset.traverse_errors(fn {message, _opt} -> message end) +      |> Enum.map_join(", ", fn {_k, v} -> v end) + +    conn +    |> put_status(422) +    |> json(%{error: error_message}) +  end + +  def errors(conn, {:error, :not_found}) do +    conn +    |> put_status(404) +    |> json(%{error: "Record not found"}) +  end +    def errors(conn, _) do      conn      |> put_status(500) @@ -1454,7 +1555,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do -    with %Activity{} = activity <- Repo.get(Activity, status_id), +    with %Activity{} = activity <- Activity.get_by_id(status_id),           true <- Visibility.visible_for_user?(activity, user) do        data =          StatusView.render( diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex new file mode 100644 index 000000000..0aae15ab9 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do +  use Pleroma.Web, :view + +  alias Pleroma.ScheduledActivity +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.MastodonAPI.ScheduledActivityView +  alias Pleroma.Web.MastodonAPI.StatusView + +  def render("index.json", %{scheduled_activities: scheduled_activities}) do +    render_many(scheduled_activities, ScheduledActivityView, "show.json") +  end + +  def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do +    %{ +      id: to_string(scheduled_activity.id), +      scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at), +      params: status_params(scheduled_activity.params) +    } +    |> with_media_attachments(scheduled_activity) +  end + +  defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do +    try do +      attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment) +      Map.put(data, :media_attachments, attachments) +    rescue +      _ -> data +    end +  end + +  defp with_media_attachments(data, _), do: data + +  defp status_params(params) do +    data = %{ +      text: params["status"], +      sensitive: params["sensitive"], +      spoiler_text: params["spoiler_text"], +      visibility: params["visibility"], +      scheduled_at: params["scheduled_at"], +      poll: params["poll"], +      in_reply_to_id: params["in_reply_to_id"] +    } + +    data = +      if media_ids = params["media_ids"] do +        Map.put(data, :media_ids, media_ids) +      else +        data +      end + +    data +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 200bb453d..4c0b53bdd 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -147,10 +147,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      content =        object        |> render_content() -      |> HTML.get_cached_scrubbed_html_for_object( +      |> HTML.get_cached_scrubbed_html_for_activity(          User.html_filter_policy(opts[:for]),          activity, -        __MODULE__ +        "mastoapi:content" +      ) + +    summary = +      (object["summary"] || "") +      |> HTML.get_cached_scrubbed_html_for_activity( +        User.html_filter_policy(opts[:for]), +        activity, +        "mastoapi:summary"        )      card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)) @@ -182,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),        pinned: pinned?(activity, user),        sensitive: sensitive, -      spoiler_text: object["summary"] || "", +      spoiler_text: summary,        visibility: get_visibility(object),        media_attachments: attachments,        mentions: mentions, diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 9b262f461..1b3721e2b 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -90,7 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    # Authenticated streams.    defp allow_request(stream, {"access_token", access_token}) when stream in @streams do      with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), -         user = %User{} <- Repo.get(User, user_id) do +         user = %User{} <- User.get_by_id(user_id) do        {:ok, user}      else        _ -> {:error, 403} diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 23bbde1a6..58385a3d1 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Metadata.Utils do      # html content comes from DB already encoded, decode first and scrub after      |> HtmlEntities.decode()      |> String.replace(~r/<br\s?\/?>/, " ") -    |> HTML.get_cached_stripped_html_for_object(object, __MODULE__) +    |> HTML.get_cached_stripped_html_for_activity(object, "metadata")      |> Formatter.demojify()      |> Formatter.truncate()    end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index f0fe3b578..afaa00242 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -6,8 +6,21 @@ defmodule Pleroma.Web.OAuth.FallbackController do    use Pleroma.Web, :controller    alias Pleroma.Web.OAuth.OAuthController -  # No user/password -  def call(conn, _) do +  def call(conn, {:register, :generic_error}) do +    conn +    |> put_status(:internal_server_error) +    |> put_flash(:error, "Unknown error, please check the details and try again.") +    |> OAuthController.registration_details(conn.params) +  end + +  def call(conn, {:register, _error}) do +    conn +    |> put_status(:unauthorized) +    |> put_flash(:error, "Invalid Username/Password") +    |> OAuthController.registration_details(conn.params) +  end + +  def call(conn, _error) do      conn      |> put_status(:unauthorized)      |> put_flash(:error, "Invalid Username/Password") diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ebb3dd253..bee7084ad 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,21 +5,46 @@  defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.Auth.Authenticator +  alias Pleroma.Web.ControllerHelper    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token    import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] +  if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) +    plug(:fetch_session)    plug(:fetch_flash)    action_fallback(Pleroma.Web.OAuth.FallbackController) -  def authorize(conn, params) do +  def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do +    if ControllerHelper.truthy_param?(params["force_login"]) do +      do_authorize(conn, params) +    else +      redirect_uri = +        if is_binary(params["redirect_uri"]) do +          params["redirect_uri"] +        else +          app = Repo.preload(token, :app).app + +          app.redirect_uris +          |> String.split() +          |> Enum.at(0) +        end + +      redirect(conn, external: redirect_uri(conn, redirect_uri)) +    end +  end + +  def authorize(conn, params), do: do_authorize(conn, params) + +  defp do_authorize(conn, params) do      app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || []      scopes = oauth_scopes(params, nil) || available_scopes @@ -35,72 +60,65 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end -  def create_authorization(conn, %{ -        "authorization" => -          %{ -            "client_id" => client_id, -            "redirect_uri" => redirect_uri -          } = auth_params -      }) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, -         %App{} = app <- Repo.get_by(App, client_id: client_id), -         true <- redirect_uri in String.split(app.redirect_uris), -         scopes <- oauth_scopes(auth_params, []), -         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, -         # Note: `scope` param is intentionally not optional in this context -         {:missing_scopes, false} <- {:missing_scopes, scopes == []}, -         {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, -         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do -      redirect_uri = -        if redirect_uri == "." do -          # Special case: Local MastodonFE -          mastodon_api_url(conn, :login) -        else -          redirect_uri -        end +  def create_authorization( +        conn, +        %{"authorization" => auth_params} = params, +        opts \\ [] +      ) do +    with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do +      after_create_authorization(conn, auth, auth_params) +    else +      error -> +        handle_create_authorization_error(conn, error, auth_params) +    end +  end -      cond do -        redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> -          render(conn, "results.html", %{ -            auth: auth -          }) +  def after_create_authorization(conn, auth, %{"redirect_uri" => redirect_uri} = auth_params) do +    redirect_uri = redirect_uri(conn, redirect_uri) + +    if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do +      render(conn, "results.html", %{ +        auth: auth +      }) +    else +      connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" +      url = "#{redirect_uri}#{connector}" +      url_params = %{:code => auth.token} -        true -> -          connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" -          url = "#{redirect_uri}#{connector}" -          url_params = %{:code => auth.token} +      url_params = +        if auth_params["state"] do +          Map.put(url_params, :state, auth_params["state"]) +        else +          url_params +        end -          url_params = -            if auth_params["state"] do -              Map.put(url_params, :state, auth_params["state"]) -            else -              url_params -            end +      url = "#{url}#{Plug.Conn.Query.encode(url_params)}" -          url = "#{url}#{Plug.Conn.Query.encode(url_params)}" +      redirect(conn, external: url) +    end +  end -          redirect(conn, external: url) -      end -    else -      {scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] -> -        # Per https://github.com/tootsuite/mastodon/blob/ -        #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 -        conn -        |> put_flash(:error, "This action is outside the authorized scopes") -        |> put_status(:unauthorized) -        |> authorize(auth_params) +  defp handle_create_authorization_error(conn, {scopes_issue, _}, auth_params) +       when scopes_issue in [:unsupported_scopes, :missing_scopes] do +    # Per https://github.com/tootsuite/mastodon/blob/ +    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 +    conn +    |> put_flash(:error, "This action is outside the authorized scopes") +    |> put_status(:unauthorized) +    |> authorize(auth_params) +  end -      {:auth_active, false} -> -        # Per https://github.com/tootsuite/mastodon/blob/ -        #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 -        conn -        |> put_flash(:error, "Your login is missing a confirmed e-mail address") -        |> put_status(:forbidden) -        |> authorize(auth_params) +  defp handle_create_authorization_error(conn, {:auth_active, false}, auth_params) do +    # Per https://github.com/tootsuite/mastodon/blob/ +    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 +    conn +    |> put_flash(:error, "Your login is missing a confirmed e-mail address") +    |> put_status(:forbidden) +    |> authorize(auth_params) +  end -      error -> -        Authenticator.handle_error(conn, error) -    end +  defp handle_create_authorization_error(conn, error, _auth_params) do +    Authenticator.handle_error(conn, error)    end    def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do @@ -108,7 +126,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           fixed_token = fix_padding(params["code"]),           %Authorization{} = auth <-             Repo.get_by(Authorization, token: fixed_token, app_id: app.id), -         %User{} = user <- Repo.get(User, auth.user_id), +         %User{} = user <- User.get_by_id(auth.user_id),           {:ok, token} <- Token.exchange_token(app, auth),           {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do        response = %{ @@ -133,9 +151,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do          conn,          %{"grant_type" => "password"} = params        ) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, +    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)},           %App{} = app <- get_app_from_request(conn, params),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, +         {:user_active, true} <- {:user_active, !user.info.deactivated},           scopes <- oauth_scopes(params, app.scopes),           [] <- scopes -- app.scopes,           true <- Enum.any?(scopes), @@ -159,6 +178,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do          |> put_status(:forbidden)          |> json(%{error: "Your login is missing a confirmed e-mail address"}) +      {:user_active, false} -> +        conn +        |> put_status(:forbidden) +        |> json(%{error: "Your account is currently disabled"}) +        _error ->          put_status(conn, 400)          |> json(%{error: "Invalid credentials"}) @@ -189,6 +213,184 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end +  @doc "Prepares OAuth request to provider for Ueberauth" +  def prepare_request(conn, %{"provider" => provider} = params) do +    scope = +      oauth_scopes(params, []) +      |> Enum.join(" ") + +    state = +      params +      |> Map.delete("scopes") +      |> Map.put("scope", scope) +      |> Poison.encode!() + +    params = +      params +      |> Map.drop(~w(scope scopes client_id redirect_uri)) +      |> Map.put("state", state) + +    # Handing the request to Ueberauth +    redirect(conn, to: o_auth_path(conn, :request, provider, params)) +  end + +  def request(conn, params) do +    message = +      if params["provider"] do +        "Unsupported OAuth provider: #{params["provider"]}." +      else +        "Bad OAuth request." +      end + +    conn +    |> put_flash(:error, message) +    |> redirect(to: "/") +  end + +  def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do +    params = callback_params(params) +    messages = for e <- Map.get(failure, :errors, []), do: e.message +    message = Enum.join(messages, "; ") + +    conn +    |> put_flash(:error, "Failed to authenticate: #{message}.") +    |> redirect(external: redirect_uri(conn, params["redirect_uri"])) +  end + +  def callback(conn, params) do +    params = callback_params(params) + +    with {:ok, registration} <- Authenticator.get_registration(conn, params) do +      user = Repo.preload(registration, :user).user +      auth_params = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + +      if user do +        create_authorization( +          conn, +          %{"authorization" => auth_params}, +          user: user +        ) +      else +        registration_params = +          Map.merge(auth_params, %{ +            "nickname" => Registration.nickname(registration), +            "email" => Registration.email(registration) +          }) + +        conn +        |> put_session(:registration_id, registration.id) +        |> registration_details(registration_params) +      end +    else +      _ -> +        conn +        |> put_flash(:error, "Failed to set up user account.") +        |> redirect(external: redirect_uri(conn, params["redirect_uri"])) +    end +  end + +  defp callback_params(%{"state" => state} = params) do +    Map.merge(params, Poison.decode!(state)) +  end + +  def registration_details(conn, params) do +    render(conn, "register.html", %{ +      client_id: params["client_id"], +      redirect_uri: params["redirect_uri"], +      state: params["state"], +      scopes: oauth_scopes(params, []), +      nickname: params["nickname"], +      email: params["email"] +    }) +  end + +  def register(conn, %{"op" => "connect"} = params) do +    authorization_params = Map.put(params, "name", params["auth_name"]) +    create_authorization_params = %{"authorization" => authorization_params} + +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {_, {:ok, auth}} <- +           {:create_authorization, do_create_authorization(conn, create_authorization_params)}, +         %User{} = user <- Repo.preload(auth, :user).user, +         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do +      conn +      |> put_session_registration_id(nil) +      |> after_create_authorization(auth, authorization_params) +    else +      {:create_authorization, error} -> +        {:register, handle_create_authorization_error(conn, error, create_authorization_params)} + +      _ -> +        {:register, :generic_error} +    end +  end + +  def register(conn, %{"op" => "register"} = params) do +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do +      conn +      |> put_session_registration_id(nil) +      |> create_authorization( +        %{ +          "authorization" => %{ +            "client_id" => params["client_id"], +            "redirect_uri" => params["redirect_uri"], +            "scopes" => oauth_scopes(params, nil) +          } +        }, +        user: user +      ) +    else +      {:error, changeset} -> +        message = +          Enum.map(changeset.errors, fn {field, {error, _}} -> +            "#{field} #{error}" +          end) +          |> Enum.join("; ") + +        message = +          String.replace( +            message, +            "ap_id has already been taken", +            "nickname has already been taken" +          ) + +        conn +        |> put_status(:forbidden) +        |> put_flash(:error, "Error: #{message}.") +        |> registration_details(params) + +      _ -> +        {:register, :generic_error} +    end +  end + +  defp do_create_authorization( +         conn, +         %{ +           "authorization" => +             %{ +               "client_id" => client_id, +               "redirect_uri" => redirect_uri +             } = auth_params +         } = params, +         user \\ nil +       ) do +    with {_, {:ok, %User{} = user}} <- +           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)}, +         %App{} = app <- Repo.get_by(App, client_id: client_id), +         true <- redirect_uri in String.split(app.redirect_uris), +         scopes <- oauth_scopes(auth_params, []), +         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, +         # Note: `scope` param is intentionally not optional in this context +         {:missing_scopes, false} <- {:missing_scopes, scopes == []}, +         {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do +      Authorization.create_authorization(app, user, scopes) +    end +  end +    # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be    # decoding it.  Investigate sometime.    defp fix_padding(token) do @@ -221,4 +423,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do        nil      end    end + +  # Special case: Local MastodonFE +  defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) + +  defp redirect_uri(_conn, redirect_uri), do: redirect_uri + +  defp get_session_registration_id(conn), do: get_session(conn, :registration_id) + +  defp put_session_registration_id(conn, registration_id), +    do: put_session(conn, :registration_id, registration_id)  end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index a8b06db36..2b5ad9b94 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do    def exchange_token(app, auth) do      with {:ok, auth} <- Authorization.use_token(auth),           true <- auth.app_id == app.id do -      create_token(app, Repo.get(User, auth.user_id), auth.scopes) +      create_token(app, User.get_by_id(auth.user_id), auth.scopes)      end    end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 863573185..2233480c5 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -19,8 +19,8 @@ defmodule Pleroma.Web.Push.Impl do    @types ["Create", "Follow", "Announce", "Like"]    @doc "Performs sending notifications for user subscriptions" -  @spec perform_send(Notification.t()) :: list(any) -  def perform_send( +  @spec perform(Notification.t()) :: list(any) | :error +  def perform(          %{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =            notif        ) @@ -50,7 +50,7 @@ defmodule Pleroma.Web.Push.Impl do      end    end -  def perform_send(_) do +  def perform(_) do      Logger.warn("Unknown notification type")      :error    end diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex index 5259e8e33..729dad02a 100644 --- a/lib/pleroma/web/push/push.ex +++ b/lib/pleroma/web/push/push.ex @@ -3,18 +3,20 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Push do -  use GenServer -    alias Pleroma.Web.Push.Impl    require Logger -  ############## -  # Client API # -  ############## +  def init do +    unless enabled() do +      Logger.warn(""" +      VAPID key pair is not found. If you wish to enabled web push, please run + +          mix web_push.gen.keypair -  def start_link do -    GenServer.start_link(__MODULE__, :ok, name: __MODULE__) +      and add the resulting output to your configuration file. +      """) +    end    end    def vapid_config do @@ -30,35 +32,5 @@ defmodule Pleroma.Web.Push do    end    def send(notification), -    do: GenServer.cast(__MODULE__, {:send, notification}) - -  #################### -  # Server Callbacks # -  #################### - -  @impl true -  def init(:ok) do -    if enabled() do -      {:ok, nil} -    else -      Logger.warn(""" -      VAPID key pair is not found. If you wish to enabled web push, please run - -          mix web_push.gen.keypair - -      and add the resulting output to your configuration file. -      """) - -      :ignore -    end -  end - -  @impl true -  def handle_cast({:send, notification}, state) do -    if enabled() do -      Impl.perform_send(notification) -    end - -    {:noreply, state} -  end +    do: PleromaJobQueue.enqueue(:web_push, Impl, [notification])  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 64e1a2bd8..0af743b80 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,16 @@  defmodule Pleroma.Web.Router do    use Pleroma.Web, :router +  pipeline :browser do +    plug(:accepts, ["html"]) +    plug(:fetch_session) +  end + +  pipeline :oauth do +    plug(:fetch_session) +    plug(Pleroma.Plugs.OAuthPlug) +  end +    pipeline :api do      plug(:accepts, ["json"])      plug(:fetch_session) @@ -105,10 +115,6 @@ defmodule Pleroma.Web.Router do      plug(:accepts, ["json", "xml"])    end -  pipeline :oauth do -    plug(:accepts, ["html", "json"]) -  end -    pipeline :pleroma_api do      plug(:accepts, ["html", "json"])    end @@ -139,8 +145,12 @@ defmodule Pleroma.Web.Router do    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do      pipe_through([:admin_api, :oauth_write]) +    post("/user/follow", AdminAPIController, :user_follow) +    post("/user/unfollow", AdminAPIController, :user_unfollow) +      get("/users", AdminAPIController, :list_users)      get("/users/:nickname", AdminAPIController, :user_show) +      delete("/user", AdminAPIController, :user_delete)      patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)      post("/user", AdminAPIController, :user_create) @@ -200,10 +210,24 @@ defmodule Pleroma.Web.Router do    end    scope "/oauth", Pleroma.Web.OAuth do -    get("/authorize", OAuthController, :authorize) +    scope [] do +      pipe_through(:oauth) +      get("/authorize", OAuthController, :authorize) +    end +      post("/authorize", OAuthController, :create_authorization)      post("/token", OAuthController, :token_exchange)      post("/revoke", OAuthController, :token_revoke) +    get("/registration_details", OAuthController, :registration_details) + +    scope [] do +      pipe_through(:browser) + +      get("/prepare_request", OAuthController, :prepare_request) +      get("/:provider", OAuthController, :request) +      get("/:provider/callback", OAuthController, :callback) +      post("/register", OAuthController, :register) +    end    end    scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -235,6 +259,9 @@ defmodule Pleroma.Web.Router do        get("/notifications", MastodonAPIController, :notifications)        get("/notifications/:id", MastodonAPIController, :get_notification) +      get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) +      get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) +        get("/lists", MastodonAPIController, :get_lists)        get("/lists/:id", MastodonAPIController, :get_list)        get("/lists/:id/accounts", MastodonAPIController, :list_accounts) @@ -272,6 +299,9 @@ defmodule Pleroma.Web.Router do        post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)        post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation) +      put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) +      delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) +        post("/media", MastodonAPIController, :upload)        put("/media/:id", MastodonAPIController, :update_media) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 592749b42..a82109f92 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.Streamer do    alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Object -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Visibility @@ -82,7 +81,7 @@ defmodule Pleroma.Web.Streamer do          _ ->            Pleroma.List.get_lists_from_activity(item)            |> Enum.filter(fn list -> -            owner = Repo.get(User, list.user_id) +            owner = User.get_by_id(list.user_id)              Visibility.visible_for_user?(item, owner)            end) diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex new file mode 100644 index 000000000..4b8fb5dae --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -0,0 +1,13 @@ +<div class="scopes-input"> +  <%= label @form, :scope, "Permissions" %> + +  <div class="scopes"> +    <%= for scope <- @available_scopes do %> +      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> +      <div class="scope"> +        <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: assigns[:scope_param] || "scope[]" %> +        <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> +      </div> +    <% end %> +  </div> +</div> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex new file mode 100644 index 000000000..85f62ca64 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -0,0 +1,13 @@ +<h2>Sign in with external provider</h2> + +<%= form_for @conn, o_auth_path(@conn, :prepare_request), [method: "get"], fn f -> %> +  <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> + +  <%= hidden_input f, :client_id, value: @client_id %> +  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> +  <%= hidden_input f, :state, value: @state %> + +    <%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %> +      <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %> +    <% end %> +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex new file mode 100644 index 000000000..126390391 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -0,0 +1,43 @@ +<%= if get_flash(@conn, :info) do %> +  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<% end %> +<%= if get_flash(@conn, :error) do %> +  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<% end %> + +<h2>Registration Details</h2> + +<p>If you'd like to register a new account, please provide the details below.</p> + +<%= form_for @conn, o_auth_path(@conn, :register), [], fn f -> %> + +<div class="input"> +  <%= label f, :nickname, "Nickname" %> +  <%= text_input f, :nickname, value: @nickname %> +</div> +<div class="input"> +  <%= label f, :email, "Email" %> +  <%= text_input f, :email, value: @email %> +</div> + +<%= submit "Proceed as new user", name: "op", value: "register" %> + +<p>Alternatively, sign in to connect to existing account.</p> + +<div class="input"> +  <%= label f, :auth_name, "Name or email" %> +  <%= text_input f, :auth_name %> +</div> +<div class="input"> +  <%= label f, :password, "Password" %> +  <%= password_input f, :password %> +</div> + +<%= submit "Proceed as existing user", name: "op", value: "connect" %> + +<%= hidden_input f, :client_id, value: @client_id %> +<%= hidden_input f, :redirect_uri, value: @redirect_uri %> +<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %> +<%= hidden_input f, :state, value: @state %> + +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 161333847..87278e636 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -4,7 +4,9 @@  <%= if get_flash(@conn, :error) do %>  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>  <% end %> +  <h2>OAuth Authorization</h2> +  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>  <div class="input">    <%= label f, :name, "Name or email" %> @@ -14,22 +16,16 @@    <%= label f, :password, "Password" %>    <%= password_input f, :password %>  </div> -<div class="scopes-input"> -<%= label f, :scope, "Permissions" %> -  <div class="scopes"> -    <%= for scope <- @available_scopes do %> -      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> -      <div class="scope"> -        <%= checkbox f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> -        <%= label f, :"scope_#{scope}", String.capitalize(scope) %> -      </div> -    <% end %> -  </div> -</div> + +<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f, scope_param: "authorization[scope][]"}) %>  <%= hidden_input f, :client_id, value: @client_id %>  <%= hidden_input f, :response_type, value: @response_type %>  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> -<%= hidden_input f, :state, value: @state%> +<%= hidden_input f, :state, value: @state %>  <%= submit "Authorize" %>  <% end %> + +<%= if Pleroma.Config.oauth_consumer_enabled?() do %> +  <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> +<% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index faa733fec..26407aebd 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    require Logger    alias Comeonin.Pbkdf2 +  alias Pleroma.Activity    alias Pleroma.Emoji    alias Pleroma.Notification    alias Pleroma.PasswordResetToken @@ -21,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def show_password_reset(conn, %{"token" => token}) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- Repo.get(User, token.user_id) do +         %User{} = user <- User.get_by_id(token.user_id) do        render(conn, "password_reset.html", %{          token: token,          user: user @@ -73,36 +74,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do -    {err, followee} = OStatus.find_or_make_user(acct) -    avatar = User.avatar_url(followee) -    name = followee.nickname -    id = followee.id - -    if !!user do -      conn -      |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) +    if is_status?(acct) do +      {:ok, object} = ActivityPub.fetch_object_from_id(acct) +      %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) +      redirect(conn, to: "/notice/#{activity_id}")      else -      conn -      |> render("follow_login.html", %{ -        error: false, -        acct: acct, -        avatar: avatar, -        name: name, -        id: id -      }) +      {err, followee} = OStatus.find_or_make_user(acct) +      avatar = User.avatar_url(followee) +      name = followee.nickname +      id = followee.id + +      if !!user do +        conn +        |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) +      else +        conn +        |> render("follow_login.html", %{ +          error: false, +          acct: acct, +          avatar: avatar, +          name: name, +          id: id +        }) +      end +    end +  end + +  defp is_status?(acct) do +    case ActivityPub.fetch_and_contain_remote_object_from_id(acct) do +      {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> +        true + +      _ -> +        false      end    end    def do_remote_follow(conn, %{          "authorization" => %{"name" => username, "password" => password, "id" => id}        }) do -    followee = Repo.get(User, id) +    followee = User.get_by_id(id)      avatar = User.avatar_url(followee)      name = followee.nickname      with %User{} = user <- User.get_cached_by_nickname(username),           true <- Pbkdf2.checkpw(password, user.password_hash), -         %User{} = _followed <- Repo.get(User, id), +         %User{} = _followed <- User.get_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -124,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do -    with %User{} = followee <- Repo.get(User, id), +    with %User{} = followee <- User.get_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -266,7 +283,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def emoji(conn, _params) do -    json(conn, Enum.into(Emoji.get_all(), %{})) +    emoji = +      Emoji.get_all() +      |> Enum.map(fn {short_code, path, tags} -> +        %{short_code => %{image_url: path, tags: String.split(tags, ",")}} +      end) + +    json(conn, emoji)    end    def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 9978c7f64..9b081a316 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    end    def delete(%User{} = user, id) do -    with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id), +    with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),           {:ok, activity} <- CommonAPI.delete(id, user) do        {:ok, activity}      end @@ -227,12 +227,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do          end        %{"screen_name" => nickname} -> -        case target = Repo.get_by(User, nickname: nickname) do -          nil -> -            {:error, "No user with such screen_name"} - -          _ -> -            {:ok, target} +        case User.get_by_nickname(nickname) do +          nil -> {:error, "No user with such screen_name"} +          target -> {:ok, target}          end        _ -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 62cce18dc..a7ec9949c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -270,7 +270,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Repo.get(Activity, id), +    with %Activity{} = activity <- Activity.get_by_id(id),           true <- Visibility.visible_for_user?(activity, user) do        conn        |> put_view(ActivityView) @@ -342,7 +342,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def get_by_id_or_ap_id(id) do -    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id) +    activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)      if activity.data["type"] == "Create" do        activity @@ -434,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def confirm_email(conn, %{"user_id" => uid, "token" => token}) do -    with %User{} = user <- Repo.get(User, uid), +    with %User{} = user <- User.get_by_id(uid),           true <- user.local,           true <- user.info.confirmation_pending,           true <- user.info.confirmation_token == token, @@ -587,7 +587,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def approve_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- Repo.get(User, uid), +         %User{} = follower <- User.get_by_id(uid),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(UserView) @@ -599,7 +599,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def deny_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- Repo.get(User, uid), +         %User{} = follower <- User.get_by_id(uid),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(UserView) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index aa1d41fa2..433322eb8 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -254,10 +254,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do      html =        content -      |> HTML.get_cached_scrubbed_html_for_object( +      |> HTML.get_cached_scrubbed_html_for_activity(          User.html_filter_policy(opts[:for]),          activity, -        __MODULE__ +        "twitterapi:content"        )        |> Formatter.emojify(object["emoji"]) @@ -265,7 +265,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do        if content do          content          |> String.replace(~r/<br\s?\/?>/, "\n") -        |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__) +        |> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")        else          ""        end @@ -41,7 +41,7 @@ defmodule Pleroma.Mixfile do    def application do      [        mod: {Pleroma.Application, []}, -      extra_applications: [:logger, :runtime_tools, :comeonin], +      extra_applications: [:logger, :runtime_tools, :comeonin, :quack],        included_applications: [:ex_syslogger]      ]    end @@ -54,6 +54,12 @@ defmodule Pleroma.Mixfile do    #    # Type `mix help deps` for examples and options.    defp deps do +    oauth_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") + +    oauth_deps = +      for s <- oauth_strategies, +          do: {String.to_atom("ueberauth_#{s}"), ">= 0.0.0"} +      [        {:phoenix, "~> 1.4.1"},        {:plug_cowboy, "~> 2.0"}, @@ -71,6 +77,7 @@ defmodule Pleroma.Mixfile do        {:calendar, "~> 0.17.4"},        {:cachex, "~> 3.0.2"},        {:httpoison, "~> 1.2.0"}, +      {:poison, "~> 3.0", override: true},        {:tesla, "~> 1.2"},        {:jason, "~> 1.0"},        {:mogrify, "~> 0.6.1"}, @@ -91,11 +98,20 @@ defmodule Pleroma.Mixfile do        {:floki, "~> 0.20.0"},        {:ex_syslogger, github: "slashmili/ex_syslogger", tag: "1.4.0"},        {:timex, "~> 3.5"}, +      {:ueberauth, "~> 0.4"},        {:auto_linker,         git: "https://git.pleroma.social/pleroma/auto_linker.git", -       ref: "94193ca5f97c1f9fdf3d1469653e2d46fac34bcd"}, -      {:pleroma_job_queue, "~> 0.2.0"} -    ] +       ref: "479dd343f4e563ff91215c8275f3b5c67e032850"}, +      {:pleroma_job_queue, "~> 0.2.0"}, +      {:telemetry, "~> 0.3"}, +      {:prometheus_ex, "~> 3.0"}, +      {:prometheus_plugs, "~> 1.1"}, +      {:prometheus_phoenix, "~> 1.2"}, +      {:prometheus_ecto, "~> 1.4"}, +      {:prometheus_process_collector, "~> 1.4"}, +      {:recon, github: "ferd/recon", tag: "2.4.0"}, +      {:quack, "~> 0.1.1"} +    ] ++ oauth_deps    end    # Aliases are shortcuts or tasks specific to the current project. @@ -1,10 +1,11 @@  %{ -  "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "94193ca5f97c1f9fdf3d1469653e2d46fac34bcd", [ref: "94193ca5f97c1f9fdf3d1469653e2d46fac34bcd"]}, +  "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"}, +  "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "479dd343f4e563ff91215c8275f3b5c67e032850", [ref: "479dd343f4e563ff91215c8275f3b5c67e032850"]},    "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "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"}, -  "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, 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"},    "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -27,7 +28,7 @@    "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},    "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},    "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, -  "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [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.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, +  "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"},    "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, @@ -39,7 +40,7 @@    "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},    "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},    "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, -  "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, +  "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},    "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},    "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},    "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, @@ -57,7 +58,15 @@    "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},    "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},    "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, +  "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"}, +  "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, +  "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, +  "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, +  "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"}, +  "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, +  "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},    "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, +  "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},    "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},    "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},    "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, @@ -66,6 +75,7 @@    "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"},    "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.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [: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"},    "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/priv/repo/migrations/20190315101315_create_registrations.exs b/priv/repo/migrations/20190315101315_create_registrations.exs new file mode 100644 index 000000000..6b28cbdd3 --- /dev/null +++ b/priv/repo/migrations/20190315101315_create_registrations.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Repo.Migrations.CreateRegistrations do +  use Ecto.Migration + +  def change do +    create table(:registrations, primary_key: false) do +      add :id, :uuid, primary_key: true +      add :user_id, references(:users, type: :uuid, on_delete: :delete_all) +      add :provider, :string +      add :uid, :string +      add :info, :map, default: %{} + +      timestamps() +    end + +    create unique_index(:registrations, [:provider, :uid]) +    create unique_index(:registrations, [:user_id, :provider, :uid]) +  end +end diff --git a/priv/repo/migrations/20190328053912_create_scheduled_activities.exs b/priv/repo/migrations/20190328053912_create_scheduled_activities.exs new file mode 100644 index 000000000..dd737e25a --- /dev/null +++ b/priv/repo/migrations/20190328053912_create_scheduled_activities.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.CreateScheduledActivities do +  use Ecto.Migration + +  def change do +    create table(:scheduled_activities) do +      add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) +      add(:scheduled_at, :naive_datetime, null: false) +      add(:params, :map, null: false) + +      timestamps() +    end + +    create(index(:scheduled_activities, [:scheduled_at])) +    create(index(:scheduled_activities, [:user_id])) +  end +end diff --git a/priv/repo/migrations/20190403131720_add_oauth_token_indexes.exs b/priv/repo/migrations/20190403131720_add_oauth_token_indexes.exs new file mode 100644 index 000000000..ebcd29389 --- /dev/null +++ b/priv/repo/migrations/20190403131720_add_oauth_token_indexes.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddOauthTokenIndexes do +  use Ecto.Migration + +  def change do +    create(unique_index(:oauth_tokens, [:token])) +    create(index(:oauth_tokens, [:app_id])) +    create(index(:oauth_tokens, [:user_id])) +  end +end diff --git a/priv/static/images/pleroma-fox-tan-smol.png b/priv/static/images/pleroma-fox-tan-smol.pngBinary files differ new file mode 100644 index 000000000..e944d0e2a --- /dev/null +++ b/priv/static/images/pleroma-fox-tan-smol.png diff --git a/priv/static/images/pleroma-fox-tan.png b/priv/static/images/pleroma-fox-tan.pngBinary files differ new file mode 100644 index 000000000..da0022ff2 --- /dev/null +++ b/priv/static/images/pleroma-fox-tan.png diff --git a/priv/static/images/pleroma-tan.png b/priv/static/images/pleroma-tan.pngBinary files differ new file mode 100644 index 000000000..6c12c8e46 --- /dev/null +++ b/priv/static/images/pleroma-tan.png diff --git a/test/emoji_test.exs b/test/emoji_test.exs new file mode 100644 index 000000000..cb1d62d00 --- /dev/null +++ b/test/emoji_test.exs @@ -0,0 +1,106 @@ +defmodule Pleroma.EmojiTest do +  use ExUnit.Case, async: true +  alias Pleroma.Emoji + +  describe "get_all/0" do +    setup do +      emoji_list = Emoji.get_all() +      {:ok, emoji_list: emoji_list} +    end + +    test "first emoji", %{emoji_list: emoji_list} do +      [emoji | _others] = emoji_list +      {code, path, tags} = emoji + +      assert tuple_size(emoji) == 3 +      assert is_binary(code) +      assert is_binary(path) +      assert is_binary(tags) +    end + +    test "random emoji", %{emoji_list: emoji_list} do +      emoji = Enum.random(emoji_list) +      {code, path, tags} = emoji + +      assert tuple_size(emoji) == 3 +      assert is_binary(code) +      assert is_binary(path) +      assert is_binary(tags) +    end +  end + +  describe "match_extra/2" do +    setup do +      groups = [ +        "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"], +        "wildcard folder": "/emoji/custom/*/file.png", +        "wildcard files": "/emoji/custom/folder/*.png", +        "special file": "/emoji/custom/special.png" +      ] + +      {:ok, groups: groups} +    end + +    test "config for list of files", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/first_file.png") +        |> to_string() + +      assert group == "list of files" +    end + +    test "config with wildcard folder", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/some_folder/file.png") +        |> to_string() + +      assert group == "wildcard folder" +    end + +    test "config with wildcard folder and subfolders", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/some_folder/another_folder/file.png") +        |> to_string() + +      assert group == "wildcard folder" +    end + +    test "config with wildcard files", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/folder/some_file.png") +        |> to_string() + +      assert group == "wildcard files" +    end + +    test "config with wildcard files and subfolders", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/folder/another_folder/some_file.png") +        |> to_string() + +      assert group == "wildcard files" +    end + +    test "config for special file", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/custom/special.png") +        |> to_string() + +      assert group == "special file" +    end + +    test "no mathing returns nil", %{groups: groups} do +      group = +        groups +        |> Emoji.match_extra("/emoji/some_undefined.png") + +      refute group +    end +  end +end diff --git a/test/fixtures/httpoison_mock/emelie.atom b/test/fixtures/httpoison_mock/emelie.atom new file mode 100644 index 000000000..ddaa1c6ca --- /dev/null +++ b/test/fixtures/httpoison_mock/emelie.atom @@ -0,0 +1,306 @@ +<?xml version="1.0"?> +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0"> +    <id>https://mastodon.social/users/emelie.atom</id> +    <title>emelie 🎨</title> +    <subtitle>23 / #Sweden / #Artist / #Equestrian / #GameDev + +If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰</subtitle> +    <updated>2019-02-04T20:22:19Z</updated> +    <logo>https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png</logo> +    <author> +        <id>https://mastodon.social/users/emelie</id> +        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> +        <uri>https://mastodon.social/users/emelie</uri> +        <name>emelie</name> +        <email>emelie@mastodon.social</email> +        <summary type="html"><p>23 / <a href="https://mastodon.social/tags/sweden" class="mention hashtag" rel="tag">#<span>Sweden</span></a> / <a href="https://mastodon.social/tags/artist" class="mention hashtag" rel="tag">#<span>Artist</span></a> / <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>Equestrian</span></a> / <a href="https://mastodon.social/tags/gamedev" class="mention hashtag" rel="tag">#<span>GameDev</span></a></p><p>If I ain&apos;t spending time with my pets, I&apos;m probably drawing. 🐴 🐱 🐰</p></summary> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie"/> +        <link rel="avatar" type="image/png" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png"/> +        <link rel="header" type="image/png" media:width="700" media:height="335" href="https://files.mastodon.social/accounts/headers/000/015/657/original/847f331f3dd9e38b.png"/> +        <poco:preferredUsername>emelie</poco:preferredUsername> +        <poco:displayName>emelie 🎨</poco:displayName> +        <poco:note>23 / #Sweden / #Artist / #Equestrian / #GameDev + +If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰</poco:note> +        <mastodon:scope>public</mastodon:scope> +    </author> +    <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie"/> +    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie.atom"/> +    <link rel="hub" href="https://mastodon.social/api/push"/> +    <link rel="salmon" href="https://mastodon.social/api/salmon/15657"/> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101850331907006641</id> +        <published>2019-04-01T09:58:50Z</published> +        <updated>2019-04-01T09:58:50Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101850331907006641"/> +        <content type="html" xml:lang="en"><p>Me: I&apos;m going to make this vital change to my world building in the morning, no way I&apos;ll forget this, it&apos;s too big of a deal<br />Also me: forgets</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101850331907006641"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17854598.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94383214:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101849626603073336</id> +        <published>2019-04-01T06:59:28Z</published> +        <updated>2019-04-01T06:59:28Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849626603073336"/> +        <content type="html" xml:lang="sv"><p><span class="h-card"><a href="https://mastodon.social/@Fergant" class="u-url mention">@<span>Fergant</span></a></span> Dom är i stort sett religiös skrift vid det här laget 👏👏</p><p>har dock bara läst svenska översättningen, kanske är dags att jag läser dom på engelska</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Fergant"/> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849626603073336"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852590.atom"/> +        <thr:in-reply-to ref="https://mastodon.social/users/Fergant/statuses/101849606513357387" href="https://mastodon.social/@Fergant/101849606513357387"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94362529:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101849580030237068</id> +        <published>2019-04-01T06:47:37Z</published> +        <updated>2019-04-01T06:47:37Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849580030237068"/> +        <content type="html" xml:lang="en"><p>What&apos;s you people&apos;s favourite fantasy books? Give me some hot tips 🌞</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849580030237068"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852464.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94362529:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101849550599949363</id> +        <published>2019-04-01T06:40:08Z</published> +        <updated>2019-04-01T06:40:08Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849550599949363"/> +        <content type="html" xml:lang="en"><p>Stick them legs out 💃 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <category term="mastocats"/> +        <link rel="enclosure" type="image/jpeg" length="516384" href="https://files.mastodon.social/media_attachments/files/013/051/707/original/125a310abe9a34aa.jpeg"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849550599949363"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852407.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94361580:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101849191533152720</id> +        <published>2019-04-01T05:08:49Z</published> +        <updated>2019-04-01T05:08:49Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849191533152720"/> +        <content type="html" xml:lang="en"><p>long 🐱 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <category term="mastocats"/> +        <link rel="enclosure" type="image/jpeg" length="305208" href="https://files.mastodon.social/media_attachments/files/013/049/940/original/f2dbbfe7de3a17d2.jpeg"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849191533152720"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17851663.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94351141:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101849165031453009</id> +        <published>2019-04-01T05:02:05Z</published> +        <updated>2019-04-01T05:02:05Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849165031453009"/> +        <content type="html" xml:lang="en"><p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <category term="mastocats"/> +        <link rel="enclosure" type="video/mp4" length="9838915" href="https://files.mastodon.social/media_attachments/files/013/049/816/original/e7831178a5e0d6d4.mp4"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849165031453009"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17851558.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94350309:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101846512530748693</id> +        <published>2019-03-31T17:47:31Z</published> +        <updated>2019-03-31T17:47:31Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101846512530748693"/> +        <content type="html" xml:lang="en"><p>Hello look at this boy having a decent haircut for once <a href="https://mastodon.social/tags/mastohorses" class="mention hashtag" rel="tag">#<span>mastohorses</span></a> <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>equestrian</span></a></p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <category term="equestrian"/> +        <category term="mastohorses"/> +        <link rel="enclosure" type="image/jpeg" length="461632" href="https://files.mastodon.social/media_attachments/files/013/033/387/original/301e8ab668cd61d2.jpeg"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101846512530748693"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17842424.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94256415:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101846181093805500</id> +        <published>2019-03-31T16:23:14Z</published> +        <updated>2019-03-31T16:23:14Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101846181093805500"/> +        <content type="html" xml:lang="en"><p>Sorry did I disturb the who-is-the-longest-cat competition ?  <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <category term="mastocats"/> +        <link rel="enclosure" type="image/jpeg" length="211384" href="https://files.mastodon.social/media_attachments/files/013/030/725/original/5b4886730cbbd25c.jpeg"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101846181093805500"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17841108.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94245239:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101845897513133849</id> +        <published>2019-03-31T15:11:07Z</published> +        <updated>2019-03-31T15:11:07Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101845897513133849"/> +        <summary xml:lang="en">more earthsea ramblings</summary> +        <content type="html" xml:lang="en"><p>I&apos;m re-watching Tales from Earthsea for the first time since I read the books, and that Therru doesn&apos;t squash Cob like a spider, as Orm Embar did is a wasted opportunity tbh</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101845897513133849"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17840088.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94232455:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101841219051533307</id> +        <published>2019-03-30T19:21:19Z</published> +        <updated>2019-03-30T19:21:19Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101841219051533307"/> +        <content type="html" xml:lang="en"><p>I gave my cats some mackerel and they ate it all in 0.3 seconds, and now they won&apos;t stop meowing for more, and I&apos;m tired plz shut up</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101841219051533307"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17826587.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94075000:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101839949762341381</id> +        <published>2019-03-30T13:58:31Z</published> +        <updated>2019-03-30T13:58:31Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101839949762341381"/> +        <content type="html" xml:lang="en"><p>yet I&apos;m  confused about this american dude with a gun, like the heck r ya doin in mah ghibli</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101839949762341381"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17821757.atom"/> +        <thr:in-reply-to ref="https://mastodon.social/users/emelie/statuses/101839928677863590" href="https://mastodon.social/@emelie/101839928677863590"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94026360:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101839928677863590</id> +        <published>2019-03-30T13:53:09Z</published> +        <updated>2019-03-30T13:53:09Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101839928677863590"/> +        <content type="html" xml:lang="en"><p>2 hours into Ni no Kuni 2 and I&apos;ve already sold my soul to this game</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101839928677863590"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17821713.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94026360:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101836329521599438</id> +        <published>2019-03-29T22:37:51Z</published> +        <updated>2019-03-29T22:37:51Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101836329521599438"/> +        <content type="html" xml:lang="en"><p>Pippi Longstocking the original one-punch /man</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101836329521599438"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17811608.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93907854:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101835905282948341</id> +        <published>2019-03-29T20:49:57Z</published> +        <updated>2019-03-29T20:49:57Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835905282948341"/> +        <content type="html" xml:lang="en"><p>I&apos;ve had so much wine I thought I had a 3rd brother</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835905282948341"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809862.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93892966:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101835878059204660</id> +        <published>2019-03-29T20:43:02Z</published> +        <updated>2019-03-29T20:43:02Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835878059204660"/> +        <content type="html" xml:lang="en"><p>ååååhhh booi</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835878059204660"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809734.atom"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93892010:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101835848050598939</id> +        <published>2019-03-29T20:35:24Z</published> +        <updated>2019-03-29T20:35:24Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835848050598939"/> +        <content type="html" xml:lang="en"><p><span class="h-card"><a href="https://thraeryn.net/@thraeryn" class="u-url mention">@<span>thraeryn</span></a></span> if I spent 1 hour and a half watching this monstrosity, I need to</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://thraeryn.net/users/thraeryn"/> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835848050598939"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809591.atom"/> +        <thr:in-reply-to ref="https://thraeryn.net/users/thraeryn/statuses/101835839202826007" href="https://thraeryn.net/@thraeryn/101835839202826007"/> +        <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93888827:objectType=Conversation"/> +    </entry> +    <entry> +        <id>https://mastodon.social/users/emelie/statuses/101835823138262290</id> +        <published>2019-03-29T20:29:04Z</published> +        <updated>2019-03-29T20:29:04Z</updated> +        <title>New status by emelie</title> +        <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> +        <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> +        <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835823138262290"/> +        <summary xml:lang="en">medical, fluids mention</summary> +        <content type="html" xml:lang="en"><p><span class="h-card"><a href="https://icosahedron.website/@Trev" class="u-url mention">@<span>Trev</span></a></span> *hugs* ✨</p></content> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://icosahedron.website/users/Trev"/> +        <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> +        <mastodon:scope>public</mastodon:scope> +        <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835823138262290"/> +        <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809468.atom"/> +        <thr:in-reply-to ref="https://icosahedron.website/users/Trev/statuses/101835812250051801" href="https://icosahedron.website/@Trev/101835812250051801"/> +        <ostatus:conversation ref="tag:icosahedron.website,2019-03-29:objectId=12220882:objectType=Conversation"/> +    </entry> +</feed> diff --git a/test/fixtures/httpoison_mock/status.emelie.json b/test/fixtures/httpoison_mock/status.emelie.json new file mode 100644 index 000000000..4aada0377 --- /dev/null +++ b/test/fixtures/httpoison_mock/status.emelie.json @@ -0,0 +1,64 @@ +{ +    "@context": [ +        "https://www.w3.org/ns/activitystreams", +        { +            "ostatus": "http://ostatus.org#", +            "atomUri": "ostatus:atomUri", +            "inReplyToAtomUri": "ostatus:inReplyToAtomUri", +            "conversation": "ostatus:conversation", +            "sensitive": "as:sensitive", +            "Hashtag": "as:Hashtag", +            "toot": "http://joinmastodon.org/ns#", +            "Emoji": "toot:Emoji", +            "focalPoint": { +                "@container": "@list", +                "@id": "toot:focalPoint" +            } +        } +    ], +    "id": "https://mastodon.social/users/emelie/statuses/101849165031453009", +    "type": "Note", +    "summary": null, +    "inReplyTo": null, +    "published": "2019-04-01T05:02:05Z", +    "url": "https://mastodon.social/@emelie/101849165031453009", +    "attributedTo": "https://mastodon.social/users/emelie", +    "to": [ +        "https://www.w3.org/ns/activitystreams#Public" +    ], +    "cc": [ +        "https://mastodon.social/users/emelie/followers" +    ], +    "sensitive": false, +    "atomUri": "https://mastodon.social/users/emelie/statuses/101849165031453009", +    "inReplyToAtomUri": null, +    "conversation": "tag:mastodon.social,2019-04-01:objectId=94350309:objectType=Conversation", +    "content": "<p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href=\"https://mastodon.social/tags/mastocats\" class=\"mention hashtag\" rel=\"tag\">#<span>mastocats</span></a></p>", +    "contentMap": { +        "en": "<p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href=\"https://mastodon.social/tags/mastocats\" class=\"mention hashtag\" rel=\"tag\">#<span>mastocats</span></a></p>" +    }, +    "attachment": [ +        { +            "type": "Document", +            "mediaType": "video/mp4", +            "url": "https://files.mastodon.social/media_attachments/files/013/049/816/original/e7831178a5e0d6d4.mp4", +            "name": null +        } +    ], +    "tag": [ +        { +            "type": "Hashtag", +            "href": "https://mastodon.social/tags/mastocats", +            "name": "#mastocats" +        } +    ], +    "replies": { +        "id": "https://mastodon.social/users/emelie/statuses/101849165031453009/replies", +        "type": "Collection", +        "first": { +            "type": "CollectionPage", +            "partOf": "https://mastodon.social/users/emelie/statuses/101849165031453009/replies", +            "items": [] +        } +    } +} diff --git a/test/fixtures/httpoison_mock/webfinger_emelie.json b/test/fixtures/httpoison_mock/webfinger_emelie.json new file mode 100644 index 000000000..0b61cb618 --- /dev/null +++ b/test/fixtures/httpoison_mock/webfinger_emelie.json @@ -0,0 +1,36 @@ +{ +    "aliases": [ +        "https://mastodon.social/@emelie", +        "https://mastodon.social/users/emelie" +    ], +    "links": [ +        { +            "href": "https://mastodon.social/@emelie", +            "rel": "http://webfinger.net/rel/profile-page", +            "type": "text/html" +        }, +        { +            "href": "https://mastodon.social/users/emelie.atom", +            "rel": "http://schemas.google.com/g/2010#updates-from", +            "type": "application/atom+xml" +        }, +        { +            "href": "https://mastodon.social/users/emelie", +            "rel": "self", +            "type": "application/activity+json" +        }, +        { +            "href": "https://mastodon.social/api/salmon/15657", +            "rel": "salmon" +        }, +        { +            "href": "data:application/magic-public-key,RSA.u3CWs1oAJPE3ZJ9sj6Ut_Mu-mTE7MOijsQc8_6c73XVVuhIEomiozJIH7l8a7S1n5SYL4UuiwcubSOi7u1bbGpYnp5TYhN-Cxvq_P80V4_ncNIPSQzS49it7nSLeG5pA21lGPDA44huquES1un6p9gSmbTwngVX9oe4MYuUeh0Z7vijjU13Llz1cRq_ZgPQPgfz-2NJf-VeXnvyDZDYxZPVBBlrMl3VoGbu0M5L8SjY35559KCZ3woIvqRolcoHXfgvJMdPcJgSZVYxlCw3dA95q9jQcn6s87CPSUs7bmYEQCrDVn5m5NER5TzwBmP4cgJl9AaDVWQtRd4jFZNTxlQ==.AQAB", +            "rel": "magic-public-key" +        }, +        { +            "rel": "http://ostatus.org/schema/1.0/subscribe", +            "template": "https://mastodon.social/authorize_interaction?uri={uri}" +        } +    ], +    "subject": "acct:emelie@mastodon.social" +} diff --git a/test/formatter_test.exs b/test/formatter_test.exs index fcdf931b7..e74985c4e 100644 --- a/test/formatter_test.exs +++ b/test/formatter_test.exs @@ -271,7 +271,9 @@ defmodule Pleroma.FormatterTest do    test "it returns the emoji used in the text" do      text = "I love :moominmamma:" -    assert Formatter.get_emoji(text) == [{"moominmamma", "/finmoji/128px/moominmamma-128.png"}] +    assert Formatter.get_emoji(text) == [ +             {"moominmamma", "/finmoji/128px/moominmamma-128.png", "Finmoji"} +           ]    end    test "it returns a nice empty result when no emojis are present" do diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs index 302662797..8b0b06772 100644 --- a/test/plugs/legacy_authentication_plug_test.exs +++ b/test/plugs/legacy_authentication_plug_test.exs @@ -47,16 +47,18 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do        |> assign(:auth_user, user)      conn = -      with_mock User, -        reset_password: fn user, %{password: password, password_confirmation: password} -> -          send(self(), :reset_password) -          {:ok, user} -        end do -        conn -        |> LegacyAuthenticationPlug.call(%{}) +      with_mocks([ +        {:crypt, [], [crypt: fn _password, password_hash -> password_hash end]}, +        {User, [], +         [ +           reset_password: fn user, %{password: password, password_confirmation: password} -> +             {:ok, user} +           end +         ]} +      ]) do +        LegacyAuthenticationPlug.call(conn, %{})        end -    assert_received :reset_password      assert conn.assigns.user == user    end diff --git a/test/registration_test.exs b/test/registration_test.exs new file mode 100644 index 000000000..6143b82c7 --- /dev/null +++ b/test/registration_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RegistrationTest do +  use Pleroma.DataCase + +  import Pleroma.Factory + +  alias Pleroma.Registration +  alias Pleroma.Repo + +  describe "generic changeset" do +    test "requires :provider, :uid" do +      registration = build(:registration, provider: nil, uid: nil) + +      cs = Registration.changeset(registration, %{}) +      refute cs.valid? + +      assert [ +               provider: {"can't be blank", [validation: :required]}, +               uid: {"can't be blank", [validation: :required]} +             ] == cs.errors +    end + +    test "ensures uniqueness of [:provider, :uid]" do +      registration = insert(:registration) +      registration2 = build(:registration, provider: registration.provider, uid: registration.uid) + +      cs = Registration.changeset(registration2, %{}) +      assert cs.valid? + +      assert {:error, +              %Ecto.Changeset{ +                errors: [ +                  uid: +                    {"has already been taken", +                     [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} +                ] +              }} = Repo.insert(cs) + +      # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed +      cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) +      assert cs2.valid? +      assert {:ok, _} = Repo.insert(cs2) + +      cs3 = Registration.changeset(registration2, %{provider: "provider2"}) +      assert cs3.valid? +      assert {:ok, _} = Repo.insert(cs3) +    end + +    test "allows `nil` :user_id (user-unbound registration)" do +      registration = build(:registration, user_id: nil) +      cs = Registration.changeset(registration, %{}) +      assert cs.valid? +      assert {:ok, _} = Repo.insert(cs) +    end +  end +end diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs new file mode 100644 index 000000000..edc7cc3f9 --- /dev/null +++ b/test/scheduled_activity_test.exs @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityTest do +  use Pleroma.DataCase +  alias Pleroma.DataCase +  alias Pleroma.ScheduledActivity +  import Pleroma.Factory + +  setup context do +    DataCase.ensure_local_uploader(context) +  end + +  describe "creation" do +    test "when daily user limit is exceeded" do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: today} +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:error, changeset} = ScheduledActivity.create(user, attrs) +      assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] +    end + +    test "when total user limit is exceeded" do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      tomorrow = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.hours(36), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) +      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) +      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) +      {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) +      assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] +    end + +    test "when scheduled_at is earlier than 5 minute from now" do +      user = insert(:user) + +      scheduled_at = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(4), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: scheduled_at} +      {:error, changeset} = ScheduledActivity.create(user, attrs) +      assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] +    end +  end +end diff --git a/test/scheduled_activity_worker_test.exs b/test/scheduled_activity_worker_test.exs new file mode 100644 index 000000000..b9c91dda6 --- /dev/null +++ b/test/scheduled_activity_worker_test.exs @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityWorkerTest do +  use Pleroma.DataCase +  alias Pleroma.ScheduledActivity +  import Pleroma.Factory + +  test "creates a status from the scheduled activity" do +    user = insert(:user) +    scheduled_activity = insert(:scheduled_activity, user: user, params: %{status: "hi"}) +    Pleroma.ScheduledActivityWorker.perform(:execute, scheduled_activity.id) + +    refute Repo.get(ScheduledActivity, scheduled_activity.id) +    activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) +    assert activity.data["object"]["content"] == "hi" +  end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 18f77f01a..ea59912cf 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -216,7 +216,7 @@ defmodule Pleroma.Factory do        redirect_uris: "https://example.com/callback",        scopes: ["read", "write", "follow", "push"],        website: "https://example.com", -      client_id: "aaabbb==", +      client_id: Ecto.UUID.generate(),        client_secret: "aaa;/&bbb"      }    end @@ -240,6 +240,16 @@ defmodule Pleroma.Factory do      }    end +  def oauth_authorization_factory do +    %Pleroma.Web.OAuth.Authorization{ +      token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false), +      scopes: ["read", "write", "follow", "push"], +      valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10), +      user: build(:user), +      app: build(:oauth_app) +    } +  end +    def push_subscription_factory do      %Pleroma.Web.Push.Subscription{        user: build(:user), @@ -257,4 +267,28 @@ defmodule Pleroma.Factory do        user: build(:user)      }    end + +  def scheduled_activity_factory do +    %Pleroma.ScheduledActivity{ +      user: build(:user), +      scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond), +      params: build(:note) |> Map.from_struct() |> Map.get(:data) +    } +  end + +  def registration_factory do +    user = insert(:user) + +    %Pleroma.Registration{ +      user: user, +      provider: "twitter", +      uid: "171799000", +      info: %{ +        "name" => "John Doe", +        "email" => "john@doe.com", +        "nickname" => "johndoe", +        "description" => "My bio" +      } +    } +  end  end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 78e8efc9d..d3b547d91 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -36,6 +36,43 @@ defmodule HttpRequestMock do       }}    end +  def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do +    {:ok, +     %Tesla.Env{ +       status: 200, +       body: File.read!("test/fixtures/httpoison_mock/status.emelie.json") +     }} +  end + +  def get("https://mastodon.social/users/emelie", _, _, _) do +    {:ok, +     %Tesla.Env{ +       status: 200, +       body: File.read!("test/fixtures/httpoison_mock/emelie.json") +     }} +  end + +  def get( +        "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie", +        _, +        _, +        _ +      ) do +    {:ok, +     %Tesla.Env{ +       status: 200, +       body: File.read!("test/fixtures/httpoison_mock/webfinger_emelie.json") +     }} +  end + +  def get("https://mastodon.social/users/emelie.atom", _, _, _) do +    {:ok, +     %Tesla.Env{ +       status: 200, +       body: File.read!("test/fixtures/httpoison_mock/emelie.atom") +     }} +  end +    def get(          "https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com",          _, diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index 7b814d171..1030bd555 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -248,4 +248,14 @@ defmodule Mix.Tasks.Pleroma.UserTest do        assert message =~ "Generated"      end    end + +  describe "running delete_activities" do +    test "activities are deleted" do +      %{nickname: nickname} = insert(:user) + +      assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) +      assert_received {:mix_shell, :info, [message]} +      assert message == "User #{nickname} statuses deleted." +    end +  end  end diff --git a/test/user_test.exs b/test/user_test.exs index 8cf2ba6ab..38712cebb 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -122,7 +122,7 @@ defmodule Pleroma.UserTest do      {:ok, user} = User.follow(user, followed) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      followed = User.get_by_ap_id(followed.ap_id)      assert followed.info.follower_count == 1 @@ -178,7 +178,7 @@ defmodule Pleroma.UserTest do      {:ok, user, _activity} = User.unfollow(user, followed) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      assert user.following == []    end @@ -188,7 +188,7 @@ defmodule Pleroma.UserTest do      {:error, _} = User.unfollow(user, user) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      assert user.following == [user.ap_id]    end @@ -200,6 +200,13 @@ defmodule Pleroma.UserTest do      refute User.following?(followed, user)    end +  test "fetches correct profile for nickname beginning with number" do +    # Use old-style integer ID to try to reproduce the problem +    user = insert(:user, %{id: 1080}) +    userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"}) +    assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname) +  end +    describe "user registration" do      @full_user_data %{        bio: "A guy", @@ -679,7 +686,7 @@ defmodule Pleroma.UserTest do        assert User.following?(blocked, blocker)        {:ok, blocker} = User.block(blocker, blocked) -      blocked = Repo.get(User, blocked.id) +      blocked = User.get_by_id(blocked.id)        assert User.blocks?(blocker, blocked) @@ -697,7 +704,7 @@ defmodule Pleroma.UserTest do        refute User.following?(blocked, blocker)        {:ok, blocker} = User.block(blocker, blocked) -      blocked = Repo.get(User, blocked.id) +      blocked = User.get_by_id(blocked.id)        assert User.blocks?(blocker, blocked) @@ -715,7 +722,7 @@ defmodule Pleroma.UserTest do        assert User.following?(blocked, blocker)        {:ok, blocker} = User.block(blocker, blocked) -      blocked = Repo.get(User, blocked.id) +      blocked = User.get_by_id(blocked.id)        assert User.blocks?(blocker, blocked) @@ -792,6 +799,16 @@ defmodule Pleroma.UserTest do      assert false == user.info.deactivated    end +  test ".delete_user_activities deletes all create activities" do +    user = insert(:user) + +    {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) +    {:ok, _} = User.delete_user_activities(user) + +    # TODO: Remove favorites, repeats, delete activities. +    refute Activity.get_by_id(activity.id) +  end +    test ".delete deactivates a user, all follow relationships and all create activities" do      user = insert(:user)      followed = insert(:user) @@ -809,9 +826,9 @@ defmodule Pleroma.UserTest do      {:ok, _} = User.delete(user) -    followed = Repo.get(User, followed.id) -    follower = Repo.get(User, follower.id) -    user = Repo.get(User, user.id) +    followed = User.get_by_id(followed.id) +    follower = User.get_by_id(follower.id) +    user = User.get_by_id(user.id)      assert user.info.deactivated @@ -820,7 +837,7 @@ defmodule Pleroma.UserTest do      # TODO: Remove favorites, repeats, delete activities. -    refute Repo.get(Activity, activity.id) +    refute Activity.get_by_id(activity.id)    end    test "get_public_key_for_ap_id fetches a user that's not in the db" do diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index a1e83b380..8dd8e7e0a 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do    alias Pleroma.Activity    alias Pleroma.Instances    alias Pleroma.Object -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ObjectView    alias Pleroma.Web.ActivityPub.UserView @@ -51,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do          |> put_req_header("accept", "application/json")          |> get("/users/#{user.nickname}") -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert json_response(conn, 200) == UserView.render("user.json", %{user: user})      end @@ -66,7 +65,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do          |> put_req_header("accept", "application/activity+json")          |> get("/users/#{user.nickname}") -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert json_response(conn, 200) == UserView.render("user.json", %{user: user})      end @@ -84,7 +83,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do          )          |> get("/users/#{user.nickname}") -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert json_response(conn, 200) == UserView.render("user.json", %{user: user})      end @@ -543,7 +542,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do        user = insert(:user)        Enum.each(1..15, fn _ -> -        user = Repo.get(User, user.id) +        user = User.get_by_id(user.id)          other_user = insert(:user)          User.follow(user, other_user)        end) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index ac5fbe0a9..17fec05b1 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -218,18 +218,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        user = insert(:user)        {:ok, _} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "1", "visibility" => "public"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "1", "visibility" => "public"})        {:ok, _} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "2", "visibility" => "unlisted"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "unlisted"})        {:ok, _} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "2", "visibility" => "private"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "private"})        {:ok, _} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "3", "visibility" => "direct"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "3", "visibility" => "direct"}) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.note_count == 2      end @@ -322,7 +322,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do      {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})      {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)      %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) -    activity_three = Repo.get(Activity, activity_three.id) +    activity_three = Activity.get_by_id(activity_three.id)      activities =        ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -380,7 +380,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do      {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})      {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)      %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) -    activity_three = Repo.get(Activity, activity_three.id) +    activity_three = Activity.get_by_id(activity_three.id)      activities =        ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -559,7 +559,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        {:ok, _, _, object} = ActivityPub.unlike(user, object)        assert object.data["like_count"] == 0 -      assert Repo.get(Activity, like_activity.id) == nil +      assert Activity.get_by_id(like_activity.id) == nil      end    end @@ -610,7 +610,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        assert unannounce_activity.data["actor"] == user.ap_id        assert unannounce_activity.data["context"] == announce_activity.data["context"] -      assert Repo.get(Activity, announce_activity.id) == nil +      assert Activity.get_by_id(announce_activity.id) == nil      end    end @@ -635,16 +635,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do      end    end -  describe "fetch the latest Follow" do -    test "fetches the latest Follow activity" do -      %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) -      follower = Repo.get_by(User, ap_id: activity.data["actor"]) -      followed = Repo.get_by(User, ap_id: activity.data["object"]) - -      assert activity == Utils.fetch_latest_follow(follower, followed) -    end -  end -    describe "fetching an object" do      test "it fetches an object" do        {:ok, object} = @@ -749,7 +739,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        assert delete.data["actor"] == note.data["actor"]        assert delete.data["object"] == note.data["object"]["id"] -      assert Repo.get(Activity, delete.id) != nil +      assert Activity.get_by_id(delete.id) != nil        assert Repo.get(Object, object.id).data["type"] == "Tombstone"      end @@ -758,23 +748,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        user = insert(:user, info: %{note_count: 10})        {:ok, a1} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "yeah", "visibility" => "public"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "public"})        {:ok, a2} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "yeah", "visibility" => "unlisted"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "unlisted"})        {:ok, a3} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "yeah", "visibility" => "private"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "private"})        {:ok, a4} = -        CommonAPI.post(Repo.get(User, user.id), %{"status" => "yeah", "visibility" => "direct"}) +        CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "direct"})        {:ok, _} = a1.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()        {:ok, _} = a2.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()        {:ok, _} = a3.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()        {:ok, _} = a4.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete() -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.note_count == 10      end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 50e8e40bd..47cffe257 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -461,7 +461,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) -      refute Repo.get(Activity, activity.id) +      refute Activity.get_by_id(activity.id)      end      test "it fails for incoming deletes with spoofed origin" do @@ -481,7 +481,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        :error = Transmogrifier.handle_incoming(data) -      assert Repo.get(Activity, activity.id) +      assert Activity.get_by_id(activity.id)      end      test "it works for incoming unannounces with an existing notice" do @@ -639,7 +639,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        assert activity.data["object"] == follow_activity.data["id"] -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        assert User.following?(follower, followed) == true      end @@ -661,7 +661,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, activity} = Transmogrifier.handle_incoming(accept_data)        assert activity.data["object"] == follow_activity.data["id"] -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        assert User.following?(follower, followed) == true      end @@ -681,7 +681,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, activity} = Transmogrifier.handle_incoming(accept_data)        assert activity.data["object"] == follow_activity.data["id"] -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        assert User.following?(follower, followed) == true      end @@ -700,7 +700,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        :error = Transmogrifier.handle_incoming(accept_data) -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        refute User.following?(follower, followed) == true      end @@ -719,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        :error = Transmogrifier.handle_incoming(accept_data) -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        refute User.following?(follower, followed) == true      end @@ -744,7 +744,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, activity} = Transmogrifier.handle_incoming(reject_data)        refute activity.local -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        assert User.following?(follower, followed) == false      end @@ -766,7 +766,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) -      follower = Repo.get(User, follower.id) +      follower = User.get_by_id(follower.id)        assert User.following?(follower, followed) == false      end @@ -1020,7 +1020,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})        assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.note_count == 1        {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") @@ -1028,13 +1028,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        assert user.info.note_count == 1        assert user.follower_address == "https://niu.moe/users/rye/followers" -      # Wait for the background task -      :timer.sleep(1000) - -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.note_count == 1 -      activity = Repo.get(Activity, activity.id) +      activity = Activity.get_by_id(activity.id)        assert user.follower_address in activity.recipients        assert %{ @@ -1057,10 +1054,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        refute "..." in activity.recipients -      unrelated_activity = Repo.get(Activity, unrelated_activity.id) +      unrelated_activity = Activity.get_by_id(unrelated_activity.id)        refute user.follower_address in unrelated_activity.recipients -      user_two = Repo.get(User, user_two.id) +      user_two = User.get_by_id(user_two.id)        assert user.follower_address in user_two.following        refute "..." in user_two.following      end diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index 2bd3ddf93..758214e68 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -1,10 +1,34 @@  defmodule Pleroma.Web.ActivityPub.UtilsTest do    use Pleroma.DataCase +  alias Pleroma.Activity +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI    import Pleroma.Factory +  describe "fetch the latest Follow" do +    test "fetches the latest Follow activity" do +      %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) +      follower = Repo.get_by(User, ap_id: activity.data["actor"]) +      followed = Repo.get_by(User, ap_id: activity.data["object"]) + +      assert activity == Utils.fetch_latest_follow(follower, followed) +    end +  end + +  describe "fetch the latest Block" do +    test "fetches the latest Block activity" do +      blocker = insert(:user) +      blocked = insert(:user) +      {:ok, activity} = ActivityPub.block(blocker, blocked) + +      assert activity == Utils.fetch_latest_block(blocker, blocked) +    end +  end +    describe "determine_explicit_mentions()" do      test "works with an object that has mentions" do        object = %{ @@ -169,4 +193,16 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do        assert Utils.fetch_ordered_collection("http://example.com/outbox", 5) == [0, 1]      end    end + +  test "make_json_ld_header/0" do +    assert Utils.make_json_ld_header() == %{ +             "@context" => [ +               "https://www.w3.org/ns/activitystreams", +               "http://localhost:4001/schemas/litepub-0.1.jsonld", +               %{ +                 "@language" => "und" +               } +             ] +           } +  end  end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 2f53416a3..ca6bd0e97 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -5,7 +5,6 @@  defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do    use Pleroma.Web.ConnCase -  alias Pleroma.Repo    alias Pleroma.User    import Pleroma.Factory @@ -75,6 +74,50 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do      end    end +  describe "/api/pleroma/admin/user/follow" do +    test "allows to force-follow another user" do +      admin = insert(:user, info: %{is_admin: true}) +      user = insert(:user) +      follower = insert(:user) + +      build_conn() +      |> assign(:user, admin) +      |> put_req_header("accept", "application/json") +      |> post("/api/pleroma/admin/user/follow", %{ +        "follower" => follower.nickname, +        "followed" => user.nickname +      }) + +      user = User.get_by_id(user.id) +      follower = User.get_by_id(follower.id) + +      assert User.following?(follower, user) +    end +  end + +  describe "/api/pleroma/admin/user/unfollow" do +    test "allows to force-unfollow another user" do +      admin = insert(:user, info: %{is_admin: true}) +      user = insert(:user) +      follower = insert(:user) + +      User.follow(follower, user) + +      build_conn() +      |> assign(:user, admin) +      |> put_req_header("accept", "application/json") +      |> post("/api/pleroma/admin/user/unfollow", %{ +        "follower" => follower.nickname, +        "followed" => user.nickname +      }) + +      user = User.get_by_id(user.id) +      follower = User.get_by_id(follower.id) + +      refute User.following?(follower, user) +    end +  end +    describe "PUT /api/pleroma/admin/users/tag" do      setup do        admin = insert(:user, info: %{is_admin: true}) @@ -101,13 +144,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        user2: user2      } do        assert json_response(conn, :no_content) -      assert Repo.get(User, user1.id).tags == ["x", "foo", "bar"] -      assert Repo.get(User, user2.id).tags == ["y", "foo", "bar"] +      assert User.get_by_id(user1.id).tags == ["x", "foo", "bar"] +      assert User.get_by_id(user2.id).tags == ["y", "foo", "bar"]      end      test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do        assert json_response(conn, :no_content) -      assert Repo.get(User, user3.id).tags == ["unchanged"] +      assert User.get_by_id(user3.id).tags == ["unchanged"]      end    end @@ -137,13 +180,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        user2: user2      } do        assert json_response(conn, :no_content) -      assert Repo.get(User, user1.id).tags == [] -      assert Repo.get(User, user2.id).tags == ["y"] +      assert User.get_by_id(user1.id).tags == [] +      assert User.get_by_id(user2.id).tags == ["y"]      end      test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do        assert json_response(conn, :no_content) -      assert Repo.get(User, user3.id).tags == ["unchanged"] +      assert User.get_by_id(user3.id).tags == ["unchanged"]      end    end @@ -213,7 +256,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do          conn          |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.deactivated == true        assert json_response(conn, :no_content)      end @@ -225,7 +268,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do          conn          |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true}) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        assert user.info.deactivated == false        assert json_response(conn, :no_content)      end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index e04b9f9b5..f0c59d5c3 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -153,4 +153,40 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do        assert conversation_id == object.id      end    end + +  describe "formats date to asctime" do +    test "when date is in ISO 8601 format" do +      date = DateTime.utc_now() |> DateTime.to_iso8601() + +      expected = +        date +        |> DateTime.from_iso8601() +        |> elem(1) +        |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") + +      assert Utils.date_to_asctime(date) == expected +    end + +    test "when date is a binary in wrong format" do +      date = DateTime.utc_now() + +      expected = "" + +      assert Utils.date_to_asctime(date) == expected +    end + +    test "when date is a Unix timestamp" do +      date = DateTime.utc_now() |> DateTime.to_unix() + +      expected = "" + +      assert Utils.date_to_asctime(date) == expected +    end + +    test "when date is nil" do +      expected = "" + +      assert Utils.date_to_asctime(nil) == expected +    end +  end  end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 5e6ee0024..9e19fb48e 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo +  alias Pleroma.ScheduledActivity    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.CommonAPI @@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =               json_response(conn_one, 200) -    assert Repo.get(Activity, id) +    assert Activity.get_by_id(id)      conn_two =        conn @@ -140,7 +141,56 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})      assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) -    assert Repo.get(Activity, id) +    assert Activity.get_by_id(id) +  end + +  test "posting a fake status", %{conn: conn} do +    user = insert(:user) + +    real_conn = +      conn +      |> assign(:user, user) +      |> post("/api/v1/statuses", %{ +        "status" => +          "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" +      }) + +    real_status = json_response(real_conn, 200) + +    assert real_status +    assert Object.get_by_ap_id(real_status["uri"]) + +    real_status = +      real_status +      |> Map.put("id", nil) +      |> Map.put("url", nil) +      |> Map.put("uri", nil) +      |> Map.put("created_at", nil) +      |> Kernel.put_in(["pleroma", "conversation_id"], nil) + +    fake_conn = +      conn +      |> assign(:user, user) +      |> post("/api/v1/statuses", %{ +        "status" => +          "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", +        "preview" => true +      }) + +    fake_status = json_response(fake_conn, 200) + +    assert fake_status +    refute Object.get_by_ap_id(fake_status["uri"]) + +    fake_status = +      fake_status +      |> Map.put("id", nil) +      |> Map.put("url", nil) +      |> Map.put("uri", nil) +      |> Map.put("created_at", nil) +      |> Kernel.put_in(["pleroma", "conversation_id"], nil) + +    assert real_status == fake_status    end    test "posting a status with OGP link preview", %{conn: conn} do @@ -155,7 +205,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        })      assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200) -    assert Repo.get(Activity, id) +    assert Activity.get_by_id(id)      Pleroma.Config.put([:rich_media, :enabled], false)    end @@ -170,7 +220,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})      assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200) -    assert activity = Repo.get(Activity, id) +    assert activity = Activity.get_by_id(id)      assert activity.recipients == [user2.ap_id, user1.ap_id]      assert activity.data["to"] == [user2.ap_id]      assert activity.data["cc"] == [] @@ -340,7 +390,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) -    activity = Repo.get(Activity, id) +    activity = Activity.get_by_id(id)      assert activity.data["context"] == replied_to.data["context"]      assert activity.data["object"]["inReplyToStatusId"] == replied_to.id @@ -356,7 +406,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) -    activity = Repo.get(Activity, id) +    activity = Activity.get_by_id(id)      assert activity    end @@ -455,7 +505,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert %{} = json_response(conn, 200) -      refute Repo.get(Activity, activity.id) +      refute Activity.get_by_id(activity.id)      end      test "when you didn't create it", %{conn: conn} do @@ -469,7 +519,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert %{"error" => _} = json_response(conn, 403) -      assert Repo.get(Activity, activity.id) == activity +      assert Activity.get_by_id(activity.id) == activity      end      test "when you're an admin or moderator", %{conn: conn} do @@ -492,8 +542,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert %{} = json_response(res_conn, 200) -      refute Repo.get(Activity, activity1.id) -      refute Repo.get(Activity, activity2.id) +      refute Activity.get_by_id(activity1.id) +      refute Activity.get_by_id(activity2.id)      end    end @@ -1163,8 +1213,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false @@ -1183,8 +1233,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false @@ -1196,8 +1246,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert relationship = json_response(conn, 200)        assert to_string(other_user.id) == relationship["id"] -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == true      end @@ -1220,7 +1270,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        conn =          build_conn() @@ -1230,8 +1280,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert relationship = json_response(conn, 200)        assert to_string(other_user.id) == relationship["id"] -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false      end @@ -1516,7 +1566,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"id" => _id, "following" => true} = json_response(conn, 200) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      conn =        build_conn() @@ -1525,7 +1575,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"id" => _id, "following" => false} = json_response(conn, 200) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      conn =        build_conn() @@ -1547,7 +1597,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"id" => _id, "muting" => true} = json_response(conn, 200) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      conn =        build_conn() @@ -1583,7 +1633,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"id" => _id, "blocking" => true} = json_response(conn, 200) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      conn =        build_conn() @@ -1940,7 +1990,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})      # Stats should count users with missing or nil `info.deactivated` value -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      info_change = Changeset.change(user.info, %{deactivated: nil})      {:ok, _user} = @@ -2316,4 +2366,323 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert link_header =~ ~r/max_id=#{notification1.id}/      end    end + +  test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do +    # Need to set an old-style integer ID to reproduce the problem +    # (these are no longer assigned to new accounts but were preserved +    # for existing accounts during the migration to flakeIDs) +    user_one = insert(:user, %{id: 1212}) +    user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) + +    resp_one = +      conn +      |> get("/api/v1/accounts/#{user_one.id}") + +    resp_two = +      conn +      |> get("/api/v1/accounts/#{user_two.nickname}") + +    resp_three = +      conn +      |> get("/api/v1/accounts/#{user_two.id}") + +    acc_one = json_response(resp_one, 200) +    acc_two = json_response(resp_two, 200) +    acc_three = json_response(resp_three, 200) +    refute acc_one == acc_two +    assert acc_two == acc_three +  end + +  describe "custom emoji" do +    test "with tags", %{conn: conn} do +      [emoji | _body] = +        conn +        |> get("/api/v1/custom_emojis") +        |> json_response(200) + +      assert Map.has_key?(emoji, "shortcode") +      assert Map.has_key?(emoji, "static_url") +      assert Map.has_key?(emoji, "tags") +      assert is_list(emoji["tags"]) +      assert Map.has_key?(emoji, "url") +      assert Map.has_key?(emoji, "visible_in_picker") +    end +  end + +  describe "index/2 redirections" do +    setup %{conn: conn} do +      session_opts = [ +        store: :cookie, +        key: "_test", +        signing_salt: "cooldude" +      ] + +      conn = +        conn +        |> Plug.Session.call(Plug.Session.init(session_opts)) +        |> fetch_session() + +      test_path = "/web/statuses/test" +      %{conn: conn, path: test_path} +    end + +    test "redirects not logged-in users to the login page", %{conn: conn, path: path} do +      conn = get(conn, path) + +      assert conn.status == 302 +      assert redirected_to(conn) == "/web/login" +    end + +    test "does not redirect logged in users to the login page", %{conn: conn, path: path} do +      token = insert(:oauth_token) + +      conn = +        conn +        |> assign(:user, token.user) +        |> put_session(:oauth_token, token.token) +        |> get(path) + +      assert conn.status == 200 +    end + +    test "saves referer path to session", %{conn: conn, path: path} do +      conn = get(conn, path) +      return_to = Plug.Conn.get_session(conn, :return_to) + +      assert return_to == path +    end + +    test "redirects to the saved path after log in", %{conn: conn, path: path} do +      app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") +      auth = insert(:oauth_authorization, app: app) + +      conn = +        conn +        |> put_session(:return_to, path) +        |> get("/web/login", %{code: auth.token}) + +      assert conn.status == 302 +      assert redirected_to(conn) == path +    end + +    test "redirects to the getting-started page when referer is not present", %{conn: conn} do +      app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") +      auth = insert(:oauth_authorization, app: app) + +      conn = get(conn, "/web/login", %{code: auth.token}) + +      assert conn.status == 302 +      assert redirected_to(conn) == "/web/getting-started" +    end +  end + +  describe "scheduled activities" do +    test "creates a scheduled activity", %{conn: conn} do +      user = insert(:user) +      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200) +      assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(scheduled_at) +      assert [] == Repo.all(Activity) +    end + +    test "creates a scheduled activity with a media attachment", %{conn: conn} do +      user = insert(:user) +      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + +      file = %Plug.Upload{ +        content_type: "image/jpg", +        path: Path.absname("test/fixtures/image.jpg"), +        filename: "an_image.jpg" +      } + +      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "media_ids" => [to_string(upload.id)], +          "status" => "scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200) +      assert %{"type" => "image"} = media_attachment +    end + +    test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", +         %{conn: conn} do +      user = insert(:user) + +      scheduled_at = +        NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "not scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"content" => "not scheduled"} = json_response(conn, 200) +      assert [] == Repo.all(ScheduledActivity) +    end + +    test "returns error when daily user limit is exceeded", %{conn: conn} do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: today} +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, attrs) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + +      assert %{"error" => "daily limit exceeded"} == json_response(conn, 422) +    end + +    test "returns error when total user limit is exceeded", %{conn: conn} do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      tomorrow = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.hours(36), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: today} +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + +      assert %{"error" => "total limit exceeded"} == json_response(conn, 422) +    end + +    test "shows scheduled activities", %{conn: conn} do +      user = insert(:user) +      scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() +      scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() +      scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() +      scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() + +      conn = +        conn +        |> assign(:user, user) + +      # min_id +      conn_res = +        conn +        |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") + +      result = json_response(conn_res, 200) +      assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + +      # since_id +      conn_res = +        conn +        |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") + +      result = json_response(conn_res, 200) +      assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result + +      # max_id +      conn_res = +        conn +        |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") + +      result = json_response(conn_res, 200) +      assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result +    end + +    test "shows a scheduled activity", %{conn: conn} do +      user = insert(:user) +      scheduled_activity = insert(:scheduled_activity, user: user) + +      res_conn = +        conn +        |> assign(:user, user) +        |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + +      assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200) +      assert scheduled_activity_id == scheduled_activity.id |> to_string() + +      res_conn = +        conn +        |> assign(:user, user) +        |> get("/api/v1/scheduled_statuses/404") + +      assert %{"error" => "Record not found"} = json_response(res_conn, 404) +    end + +    test "updates a scheduled activity", %{conn: conn} do +      user = insert(:user) +      scheduled_activity = insert(:scheduled_activity, user: user) + +      new_scheduled_at = +        NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + +      res_conn = +        conn +        |> assign(:user, user) +        |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ +          scheduled_at: new_scheduled_at +        }) + +      assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200) +      assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) + +      res_conn = +        conn +        |> assign(:user, user) +        |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + +      assert %{"error" => "Record not found"} = json_response(res_conn, 404) +    end + +    test "deletes a scheduled activity", %{conn: conn} do +      user = insert(:user) +      scheduled_activity = insert(:scheduled_activity, user: user) + +      res_conn = +        conn +        |> assign(:user, user) +        |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + +      assert %{} = json_response(res_conn, 200) +      assert nil == Repo.get(ScheduledActivity, scheduled_activity.id) + +      res_conn = +        conn +        |> assign(:user, user) +        |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + +      assert %{"error" => "Record not found"} = json_response(res_conn, 404) +    end +  end  end diff --git a/test/web/mastodon_api/notification_view_test.exs b/test/web/mastodon_api/notification_view_test.exs index b826a7e61..f2c1eb76c 100644 --- a/test/web/mastodon_api/notification_view_test.exs +++ b/test/web/mastodon_api/notification_view_test.exs @@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do      mentioned_user = insert(:user)      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{mentioned_user.nickname}"})      {:ok, [notification]} = Notification.create_notifications(activity) -    user = Repo.get(User, user.id) +    user = User.get_by_id(user.id)      expected = %{        id: to_string(notification.id), @@ -44,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do      {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})      {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user)      {:ok, [notification]} = Notification.create_notifications(favorite_activity) -    create_activity = Repo.get(Activity, create_activity.id) +    create_activity = Activity.get_by_id(create_activity.id)      expected = %{        id: to_string(notification.id), @@ -66,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do      {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})      {:ok, reblog_activity, _object} = CommonAPI.repeat(create_activity.id, another_user)      {:ok, [notification]} = Notification.create_notifications(reblog_activity) -    reblog_activity = Repo.get(Activity, create_activity.id) +    reblog_activity = Activity.get_by_id(create_activity.id)      expected = %{        id: to_string(notification.id), diff --git a/test/web/mastodon_api/scheduled_activity_view_test.exs b/test/web/mastodon_api/scheduled_activity_view_test.exs new file mode 100644 index 000000000..ecbb855d4 --- /dev/null +++ b/test/web/mastodon_api/scheduled_activity_view_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do +  use Pleroma.DataCase +  alias Pleroma.ScheduledActivity +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.MastodonAPI.ScheduledActivityView +  alias Pleroma.Web.MastodonAPI.StatusView +  import Pleroma.Factory + +  test "A scheduled activity with a media attachment" do +    user = insert(:user) +    {:ok, activity} = CommonAPI.post(user, %{"status" => "hi"}) + +    scheduled_at = +      NaiveDateTime.utc_now() +      |> NaiveDateTime.add(:timer.minutes(10), :millisecond) +      |> NaiveDateTime.to_iso8601() + +    file = %Plug.Upload{ +      content_type: "image/jpg", +      path: Path.absname("test/fixtures/image.jpg"), +      filename: "an_image.jpg" +    } + +    {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + +    attrs = %{ +      params: %{ +        "media_ids" => [upload.id], +        "status" => "hi", +        "sensitive" => true, +        "spoiler_text" => "spoiler", +        "visibility" => "unlisted", +        "in_reply_to_id" => to_string(activity.id) +      }, +      scheduled_at: scheduled_at +    } + +    {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) +    result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) + +    expected = %{ +      id: to_string(scheduled_activity.id), +      media_attachments: +        %{"media_ids" => [upload.id]} +        |> Utils.attachments_from_ids() +        |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), +      params: %{ +        in_reply_to_id: to_string(activity.id), +        media_ids: [upload.id], +        poll: nil, +        scheduled_at: nil, +        sensitive: true, +        spoiler_text: "spoiler", +        text: "hi", +        visibility: "unlisted" +      }, +      scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) +    } + +    assert expected == result +  end +end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index e1c9b2c8f..8db92ac16 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -175,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do      status = StatusView.render("status.json", %{activity: activity}) -    actor = Repo.get_by(User, ap_id: activity.actor) +    actor = User.get_by_ap_id(activity.actor)      assert status.mentions ==               Enum.map([user, actor], fn u -> AccountView.render("mention.json", %{user: u}) end) diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 84ec7b4ee..ac7843f9b 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -5,266 +5,676 @@  defmodule Pleroma.Web.OAuth.OAuthControllerTest do    use Pleroma.Web.ConnCase    import Pleroma.Factory +  import Mock +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token -  test "redirects with oauth authorization" do -    user = insert(:user) -    app = insert(:oauth_app, scopes: ["read", "write", "follow"]) +  @session_opts [ +    store: :cookie, +    key: "_test", +    signing_salt: "cooldude" +  ] + +  describe "in OAuth consumer mode, " do +    setup do +      oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies] +      oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path) +      Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook)) + +      on_exit(fn -> +        Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies) +      end) + +      [ +        app: insert(:oauth_app), +        conn: +          build_conn() +          |> Plug.Session.call(Plug.Session.init(@session_opts)) +          |> fetch_session() +      ] +    end -    conn = -      build_conn() -      |> post("/oauth/authorize", %{ -        "authorization" => %{ -          "name" => user.nickname, -          "password" => "test", -          "client_id" => app.client_id, -          "redirect_uri" => app.redirect_uris, -          "scope" => "read write", -          "state" => "statepassed" -        } -      }) +    test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ +      app: app, +      conn: conn +    } do +      conn = +        get( +          conn, +          "/oauth/authorize", +          %{ +            "response_type" => "code", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "scope" => "read" +          } +        ) + +      assert response = html_response(conn, 200) +      assert response =~ "Sign in with Twitter" +      assert response =~ o_auth_path(conn, :prepare_request) +    end + +    test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ +      app: app, +      conn: conn +    } do +      conn = +        get( +          conn, +          "/oauth/prepare_request", +          %{ +            "provider" => "twitter", +            "scope" => "read follow", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "a_state" +          } +        ) + +      assert response = html_response(conn, 302) + +      redirect_query = URI.parse(redirected_to(conn)).query +      assert %{"state" => state_param} = URI.decode_query(redirect_query) +      assert {:ok, state_components} = Poison.decode(state_param) + +      expected_client_id = app.client_id +      expected_redirect_uri = app.redirect_uris + +      assert %{ +               "scope" => "read follow", +               "client_id" => ^expected_client_id, +               "redirect_uri" => ^expected_redirect_uri, +               "state" => "a_state" +             } = state_components +    end + +    test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", +         %{app: app, conn: conn} do +      registration = insert(:registration) + +      state_params = %{ +        "scope" => Enum.join(app.scopes, " "), +        "client_id" => app.client_id, +        "redirect_uri" => app.redirect_uris, +        "state" => "" +      } + +      with_mock Pleroma.Web.Auth.Authenticator, +        get_registration: fn _, _ -> {:ok, registration} end do +        conn = +          get( +            conn, +            "/oauth/twitter/callback", +            %{ +              "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", +              "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", +              "provider" => "twitter", +              "state" => Poison.encode!(state_params) +            } +          ) + +        assert response = html_response(conn, 302) +        assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ +      end +    end + +    test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", +         %{app: app, conn: conn} do +      registration = insert(:registration, user: nil) + +      state_params = %{ +        "scope" => "read write", +        "client_id" => app.client_id, +        "redirect_uri" => app.redirect_uris, +        "state" => "a_state" +      } + +      with_mock Pleroma.Web.Auth.Authenticator, +        get_registration: fn _, _ -> {:ok, registration} end do +        conn = +          get( +            conn, +            "/oauth/twitter/callback", +            %{ +              "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", +              "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", +              "provider" => "twitter", +              "state" => Poison.encode!(state_params) +            } +          ) + +        assert response = html_response(conn, 200) +        assert response =~ ~r/name="op" type="submit" value="register"/ +        assert response =~ ~r/name="op" type="submit" value="connect"/ +        assert response =~ Registration.email(registration) +        assert response =~ Registration.nickname(registration) +      end +    end -    target = redirected_to(conn) -    assert target =~ app.redirect_uris +    test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ +      app: app, +      conn: conn +    } do +      state_params = %{ +        "scope" => Enum.join(app.scopes, " "), +        "client_id" => app.client_id, +        "redirect_uri" => app.redirect_uris, +        "state" => "" +      } + +      conn = +        conn +        |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) +        |> get( +          "/oauth/twitter/callback", +          %{ +            "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", +            "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", +            "provider" => "twitter", +            "state" => Poison.encode!(state_params) +          } +        ) + +      assert response = html_response(conn, 302) +      assert redirected_to(conn) == app.redirect_uris +      assert get_flash(conn, :error) == "Failed to authenticate: (error description)." +    end -    query = URI.parse(target).query |> URI.query_decoder() |> Map.new() +    test "GET /oauth/registration_details renders registration details form", %{ +      app: app, +      conn: conn +    } do +      conn = +        get( +          conn, +          "/oauth/registration_details", +          %{ +            "scopes" => app.scopes, +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "a_state", +            "nickname" => nil, +            "email" => "john@doe.com" +          } +        ) + +      assert response = html_response(conn, 200) +      assert response =~ ~r/name="op" type="submit" value="register"/ +      assert response =~ ~r/name="op" type="submit" value="connect"/ +    end -    assert %{"state" => "statepassed", "code" => code} = query -    auth = Repo.get_by(Authorization, token: code) -    assert auth -    assert auth.scopes == ["read", "write"] +    test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", +         %{ +           app: app, +           conn: conn +         } do +      registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + +      conn = +        conn +        |> put_session(:registration_id, registration.id) +        |> post( +          "/oauth/register", +          %{ +            "op" => "register", +            "scopes" => app.scopes, +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "a_state", +            "nickname" => "availablenick", +            "email" => "available@email.com" +          } +        ) + +      assert response = html_response(conn, 302) +      assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ +    end + +    test "with invalid params, POST /oauth/register?op=register renders registration_details page", +         %{ +           app: app, +           conn: conn +         } do +      another_user = insert(:user) +      registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + +      params = %{ +        "op" => "register", +        "scopes" => app.scopes, +        "client_id" => app.client_id, +        "redirect_uri" => app.redirect_uris, +        "state" => "a_state", +        "nickname" => "availablenickname", +        "email" => "available@email.com" +      } + +      for {bad_param, bad_param_value} <- +            [{"nickname", another_user.nickname}, {"email", another_user.email}] do +        bad_params = Map.put(params, bad_param, bad_param_value) + +        conn = +          conn +          |> put_session(:registration_id, registration.id) +          |> post("/oauth/register", bad_params) + +        assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ +        assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." +      end +    end + +    test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", +         %{ +           app: app, +           conn: conn +         } do +      user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword")) +      registration = insert(:registration, user: nil) + +      conn = +        conn +        |> put_session(:registration_id, registration.id) +        |> post( +          "/oauth/register", +          %{ +            "op" => "connect", +            "scopes" => app.scopes, +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "a_state", +            "auth_name" => user.nickname, +            "password" => "testpassword" +          } +        ) + +      assert response = html_response(conn, 302) +      assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ +    end + +    test "with invalid params, POST /oauth/register?op=connect renders registration_details page", +         %{ +           app: app, +           conn: conn +         } do +      user = insert(:user) +      registration = insert(:registration, user: nil) + +      params = %{ +        "op" => "connect", +        "scopes" => app.scopes, +        "client_id" => app.client_id, +        "redirect_uri" => app.redirect_uris, +        "state" => "a_state", +        "auth_name" => user.nickname, +        "password" => "wrong password" +      } + +      conn = +        conn +        |> put_session(:registration_id, registration.id) +        |> post("/oauth/register", params) + +      assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ +      assert get_flash(conn, :error) == "Invalid Username/Password" +    end    end -  test "returns 401 for wrong credentials", %{conn: conn} do -    user = insert(:user) -    app = insert(:oauth_app) +  describe "GET /oauth/authorize" do +    setup do +      [ +        app: insert(:oauth_app, redirect_uris: "https://redirect.url"), +        conn: +          build_conn() +          |> Plug.Session.call(Plug.Session.init(@session_opts)) +          |> fetch_session() +      ] +    end -    result = -      conn -      |> post("/oauth/authorize", %{ -        "authorization" => %{ -          "name" => user.nickname, -          "password" => "wrong", -          "client_id" => app.client_id, -          "redirect_uri" => app.redirect_uris, -          "state" => "statepassed", -          "scope" => Enum.join(app.scopes, " ") -        } -      }) -      |> html_response(:unauthorized) - -    # Keep the details -    assert result =~ app.client_id -    assert result =~ app.redirect_uris - -    # Error message -    assert result =~ "Invalid Username/Password" +    test "renders authentication page", %{app: app, conn: conn} do +      conn = +        get( +          conn, +          "/oauth/authorize", +          %{ +            "response_type" => "code", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "scope" => "read" +          } +        ) + +      assert html_response(conn, 200) =~ ~s(type="submit") +    end + +    test "renders authentication page if user is already authenticated but `force_login` is tru-ish", +         %{app: app, conn: conn} do +      token = insert(:oauth_token, app_id: app.id) + +      conn = +        conn +        |> put_session(:oauth_token, token.token) +        |> get( +          "/oauth/authorize", +          %{ +            "response_type" => "code", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "scope" => "read", +            "force_login" => "true" +          } +        ) + +      assert html_response(conn, 200) =~ ~s(type="submit") +    end + +    test "redirects to app if user is already authenticated", %{app: app, conn: conn} do +      token = insert(:oauth_token, app_id: app.id) + +      conn = +        conn +        |> put_session(:oauth_token, token.token) +        |> get( +          "/oauth/authorize", +          %{ +            "response_type" => "code", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "scope" => "read" +          } +        ) + +      assert redirected_to(conn) == "https://redirect.url" +    end    end -  test "returns 401 for missing scopes", %{conn: conn} do -    user = insert(:user) -    app = insert(:oauth_app) +  describe "POST /oauth/authorize" do +    test "redirects with oauth authorization" do +      user = insert(:user) +      app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + +      conn = +        build_conn() +        |> post("/oauth/authorize", %{ +          "authorization" => %{ +            "name" => user.nickname, +            "password" => "test", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "scope" => "read write", +            "state" => "statepassed" +          } +        }) + +      target = redirected_to(conn) +      assert target =~ app.redirect_uris + +      query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + +      assert %{"state" => "statepassed", "code" => code} = query +      auth = Repo.get_by(Authorization, token: code) +      assert auth +      assert auth.scopes == ["read", "write"] +    end -    result = -      conn -      |> post("/oauth/authorize", %{ -        "authorization" => %{ -          "name" => user.nickname, -          "password" => "test", -          "client_id" => app.client_id, -          "redirect_uri" => app.redirect_uris, -          "state" => "statepassed", -          "scope" => "" -        } -      }) -      |> html_response(:unauthorized) - -    # Keep the details -    assert result =~ app.client_id -    assert result =~ app.redirect_uris - -    # Error message -    assert result =~ "This action is outside the authorized scopes" +    test "returns 401 for wrong credentials", %{conn: conn} do +      user = insert(:user) +      app = insert(:oauth_app) + +      result = +        conn +        |> post("/oauth/authorize", %{ +          "authorization" => %{ +            "name" => user.nickname, +            "password" => "wrong", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "statepassed", +            "scope" => Enum.join(app.scopes, " ") +          } +        }) +        |> html_response(:unauthorized) + +      # Keep the details +      assert result =~ app.client_id +      assert result =~ app.redirect_uris + +      # Error message +      assert result =~ "Invalid Username/Password" +    end + +    test "returns 401 for missing scopes", %{conn: conn} do +      user = insert(:user) +      app = insert(:oauth_app) + +      result = +        conn +        |> post("/oauth/authorize", %{ +          "authorization" => %{ +            "name" => user.nickname, +            "password" => "test", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "statepassed", +            "scope" => "" +          } +        }) +        |> html_response(:unauthorized) + +      # Keep the details +      assert result =~ app.client_id +      assert result =~ app.redirect_uris + +      # Error message +      assert result =~ "This action is outside the authorized scopes" +    end + +    test "returns 401 for scopes beyond app scopes", %{conn: conn} do +      user = insert(:user) +      app = insert(:oauth_app, scopes: ["read", "write"]) + +      result = +        conn +        |> post("/oauth/authorize", %{ +          "authorization" => %{ +            "name" => user.nickname, +            "password" => "test", +            "client_id" => app.client_id, +            "redirect_uri" => app.redirect_uris, +            "state" => "statepassed", +            "scope" => "read write follow" +          } +        }) +        |> html_response(:unauthorized) + +      # Keep the details +      assert result =~ app.client_id +      assert result =~ app.redirect_uris + +      # Error message +      assert result =~ "This action is outside the authorized scopes" +    end    end -  test "returns 401 for scopes beyond app scopes", %{conn: conn} do -    user = insert(:user) -    app = insert(:oauth_app, scopes: ["read", "write"]) +  describe "POST /oauth/token" do +    test "issues a token for an all-body request" do +      user = insert(:user) +      app = insert(:oauth_app, scopes: ["read", "write"]) -    result = -      conn -      |> post("/oauth/authorize", %{ -        "authorization" => %{ -          "name" => user.nickname, -          "password" => "test", -          "client_id" => app.client_id, +      {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + +      conn = +        build_conn() +        |> post("/oauth/token", %{ +          "grant_type" => "authorization_code", +          "code" => auth.token,            "redirect_uri" => app.redirect_uris, -          "state" => "statepassed", -          "scope" => "read write follow" -        } -      }) -      |> html_response(:unauthorized) - -    # Keep the details -    assert result =~ app.client_id -    assert result =~ app.redirect_uris - -    # Error message -    assert result =~ "This action is outside the authorized scopes" -  end +          "client_id" => app.client_id, +          "client_secret" => app.client_secret +        }) -  test "issues a token for an all-body request" do -    user = insert(:user) -    app = insert(:oauth_app, scopes: ["read", "write"]) +      assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) -    {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) +      token = Repo.get_by(Token, token: token) +      assert token +      assert token.scopes == auth.scopes +      assert user.ap_id == ap_id +    end -    conn = -      build_conn() -      |> post("/oauth/token", %{ -        "grant_type" => "authorization_code", -        "code" => auth.token, -        "redirect_uri" => app.redirect_uris, -        "client_id" => app.client_id, -        "client_secret" => app.client_secret -      }) +    test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do +      password = "testpassword" +      user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) -    assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) +      app = insert(:oauth_app, scopes: ["read", "write"]) -    token = Repo.get_by(Token, token: token) -    assert token -    assert token.scopes == auth.scopes -    assert user.ap_id == ap_id -  end +      # Note: "scope" param is intentionally omitted +      conn = +        build_conn() +        |> post("/oauth/token", %{ +          "grant_type" => "password", +          "username" => user.nickname, +          "password" => password, +          "client_id" => app.client_id, +          "client_secret" => app.client_secret +        }) -  test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do -    password = "testpassword" -    user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) +      assert %{"access_token" => token} = json_response(conn, 200) -    app = insert(:oauth_app, scopes: ["read", "write"]) +      token = Repo.get_by(Token, token: token) +      assert token +      assert token.scopes == app.scopes +    end -    # Note: "scope" param is intentionally omitted -    conn = -      build_conn() -      |> post("/oauth/token", %{ -        "grant_type" => "password", -        "username" => user.nickname, -        "password" => password, -        "client_id" => app.client_id, -        "client_secret" => app.client_secret -      }) +    test "issues a token for request with HTTP basic auth client credentials" do +      user = insert(:user) +      app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) -    assert %{"access_token" => token} = json_response(conn, 200) +      {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) +      assert auth.scopes == ["scope1", "scope2"] -    token = Repo.get_by(Token, token: token) -    assert token -    assert token.scopes == app.scopes -  end +      app_encoded = +        (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) +        |> Base.encode64() -  test "issues a token for request with HTTP basic auth client credentials" do -    user = insert(:user) -    app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) +      conn = +        build_conn() +        |> put_req_header("authorization", "Basic " <> app_encoded) +        |> post("/oauth/token", %{ +          "grant_type" => "authorization_code", +          "code" => auth.token, +          "redirect_uri" => app.redirect_uris +        }) -    {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) -    assert auth.scopes == ["scope1", "scope2"] +      assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) -    app_encoded = -      (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) -      |> Base.encode64() +      assert scope == "scope1 scope2" -    conn = -      build_conn() -      |> put_req_header("authorization", "Basic " <> app_encoded) -      |> post("/oauth/token", %{ -        "grant_type" => "authorization_code", -        "code" => auth.token, -        "redirect_uri" => app.redirect_uris -      }) +      token = Repo.get_by(Token, token: token) +      assert token +      assert token.scopes == ["scope1", "scope2"] +    end -    assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) +    test "rejects token exchange with invalid client credentials" do +      user = insert(:user) +      app = insert(:oauth_app) -    assert scope == "scope1 scope2" +      {:ok, auth} = Authorization.create_authorization(app, user) -    token = Repo.get_by(Token, token: token) -    assert token -    assert token.scopes == ["scope1", "scope2"] -  end +      conn = +        build_conn() +        |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") +        |> post("/oauth/token", %{ +          "grant_type" => "authorization_code", +          "code" => auth.token, +          "redirect_uri" => app.redirect_uris +        }) -  test "rejects token exchange with invalid client credentials" do -    user = insert(:user) -    app = insert(:oauth_app) +      assert resp = json_response(conn, 400) +      assert %{"error" => _} = resp +      refute Map.has_key?(resp, "access_token") +    end -    {:ok, auth} = Authorization.create_authorization(app, user) +    test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do +      setting = Pleroma.Config.get([:instance, :account_activation_required]) -    conn = -      build_conn() -      |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") -      |> post("/oauth/token", %{ -        "grant_type" => "authorization_code", -        "code" => auth.token, -        "redirect_uri" => app.redirect_uris -      }) +      unless setting do +        Pleroma.Config.put([:instance, :account_activation_required], true) +        on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) +      end -    assert resp = json_response(conn, 400) -    assert %{"error" => _} = resp -    refute Map.has_key?(resp, "access_token") -  end +      password = "testpassword" +      user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) +      info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) -  test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do -    setting = Pleroma.Config.get([:instance, :account_activation_required]) +      {:ok, user} = +        user +        |> Ecto.Changeset.change() +        |> Ecto.Changeset.put_embed(:info, info_change) +        |> Repo.update() -    unless setting do -      Pleroma.Config.put([:instance, :account_activation_required], true) -      on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) -    end +      refute Pleroma.User.auth_active?(user) -    password = "testpassword" -    user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) -    info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) +      app = insert(:oauth_app) -    {:ok, user} = -      user -      |> Ecto.Changeset.change() -      |> Ecto.Changeset.put_embed(:info, info_change) -      |> Repo.update() +      conn = +        build_conn() +        |> post("/oauth/token", %{ +          "grant_type" => "password", +          "username" => user.nickname, +          "password" => password, +          "client_id" => app.client_id, +          "client_secret" => app.client_secret +        }) -    refute Pleroma.User.auth_active?(user) +      assert resp = json_response(conn, 403) +      assert %{"error" => _} = resp +      refute Map.has_key?(resp, "access_token") +    end -    app = insert(:oauth_app) +    test "rejects token exchange for valid credentials belonging to deactivated user" do +      password = "testpassword" -    conn = -      build_conn() -      |> post("/oauth/token", %{ -        "grant_type" => "password", -        "username" => user.nickname, -        "password" => password, -        "client_id" => app.client_id, -        "client_secret" => app.client_secret -      }) +      user = +        insert(:user, +          password_hash: Comeonin.Pbkdf2.hashpwsalt(password), +          info: %{deactivated: true} +        ) -    assert resp = json_response(conn, 403) -    assert %{"error" => _} = resp -    refute Map.has_key?(resp, "access_token") -  end +      app = insert(:oauth_app) -  test "rejects an invalid authorization code" do -    app = insert(:oauth_app) +      conn = +        build_conn() +        |> post("/oauth/token", %{ +          "grant_type" => "password", +          "username" => user.nickname, +          "password" => password, +          "client_id" => app.client_id, +          "client_secret" => app.client_secret +        }) -    conn = -      build_conn() -      |> post("/oauth/token", %{ -        "grant_type" => "authorization_code", -        "code" => "Imobviouslyinvalid", -        "redirect_uri" => app.redirect_uris, -        "client_id" => app.client_id, -        "client_secret" => app.client_secret -      }) +      assert resp = json_response(conn, 403) +      assert %{"error" => _} = resp +      refute Map.has_key?(resp, "access_token") +    end + +    test "rejects an invalid authorization code" do +      app = insert(:oauth_app) -    assert resp = json_response(conn, 400) -    assert %{"error" => _} = json_response(conn, 400) -    refute Map.has_key?(resp, "access_token") +      conn = +        build_conn() +        |> post("/oauth/token", %{ +          "grant_type" => "authorization_code", +          "code" => "Imobviouslyinvalid", +          "redirect_uri" => app.redirect_uris, +          "client_id" => app.client_id, +          "client_secret" => app.client_secret +        }) + +      assert resp = json_response(conn, 400) +      assert %{"error" => _} = json_response(conn, 400) +      refute Map.has_key?(resp, "access_token") +    end    end  end diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index 5cb135b4c..a4bb68c4d 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -116,10 +116,10 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do      {:ok, announce, _object} = ActivityPub.announce(user, object) -    announce = Repo.get(Activity, announce.id) +    announce = Activity.get_by_id(announce.id)      note_user = User.get_cached_by_ap_id(note.data["actor"]) -    note = Repo.get(Activity, note.id) +    note = Activity.get_by_id(note.id)      note_xml =        ActivityRepresenter.to_simple_form(note, note_user, true) diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs index 412d894fd..ca6e61339 100644 --- a/test/web/ostatus/incoming_documents/delete_handling_test.exs +++ b/test/web/ostatus/incoming_documents/delete_handling_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Web.OStatus.DeleteHandlingTest do    alias Pleroma.Activity    alias Pleroma.Object -  alias Pleroma.Repo    alias Pleroma.Web.OStatus    setup do @@ -32,10 +31,10 @@ defmodule Pleroma.Web.OStatus.DeleteHandlingTest do        {:ok, [delete]} = OStatus.handle_incoming(incoming) -      refute Repo.get(Activity, note.id) -      refute Repo.get(Activity, like.id) +      refute Activity.get_by_id(note.id) +      refute Activity.get_by_id(like.id)        assert Object.get_by_ap_id(note.data["object"]["id"]).data["type"] == "Tombstone" -      assert Repo.get(Activity, second_note.id) +      assert Activity.get_by_id(second_note.id)        assert Object.get_by_ap_id(second_note.data["object"]["id"])        assert delete.data["type"] == "Delete" diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index 76b90e186..9fd100f63 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -154,7 +154,7 @@ defmodule Pleroma.Web.OStatusTest do      assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"]      refute activity.local -    retweeted_activity = Repo.get(Activity, retweeted_activity.id) +    retweeted_activity = Activity.get_by_id(retweeted_activity.id)      assert retweeted_activity.data["type"] == "Create"      assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"      refute retweeted_activity.local @@ -181,7 +181,7 @@ defmodule Pleroma.Web.OStatusTest do      assert user.ap_id in activity.data["to"]      refute activity.local -    retweeted_activity = Repo.get(Activity, retweeted_activity.id) +    retweeted_activity = Activity.get_by_id(retweeted_activity.id)      assert note_activity.id == retweeted_activity.id      assert retweeted_activity.data["type"] == "Create"      assert retweeted_activity.data["actor"] == user.ap_id @@ -344,7 +344,7 @@ defmodule Pleroma.Web.OStatusTest do        {:ok, user} = OStatus.find_or_make_user(uri) -      user = Repo.get(Pleroma.User, user.id) +      user = Pleroma.User.get_by_id(user.id)        assert user.name == "Constance Variable"        assert user.nickname == "lambadalambda@social.heldscal.la"        assert user.local == false diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 3f9f3d809..6bac2c9f6 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -64,17 +64,19 @@ defmodule Pleroma.Web.Push.ImplTest do          }        ) -    assert Impl.perform_send(notif) == [:ok, :ok] +    assert Impl.perform(notif) == [:ok, :ok]    end +  @tag capture_log: true    test "returns error if notif does not match " do -    assert Impl.perform_send(%{}) == :error +    assert Impl.perform(%{}) == :error    end    test "successful message sending" do      assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok    end +  @tag capture_log: true    test "fail message sending" do      assert Impl.push_message(               @message, diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 265e1abbd..35503259b 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -99,7 +99,7 @@ defmodule Pleroma.Web.Salmon.SalmonTest do      }      {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]}) -    user = Repo.get_by(User, ap_id: activity.data["actor"]) +    user = User.get_by_ap_id(activity.data["actor"])      {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)      poster = fn url, _data, _headers -> diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 083540017..72b7ea85e 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -719,7 +719,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/friendships/create.json", %{user_id: followed.id}) -      current_user = Repo.get(User, current_user.id) +      current_user = User.get_by_id(current_user.id)        assert User.ap_followers(followed) in current_user.following        assert json_response(conn, 200) == @@ -734,8 +734,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/friendships/create.json", %{user_id: followed.id}) -      current_user = Repo.get(User, current_user.id) -      followed = Repo.get(User, followed.id) +      current_user = User.get_by_id(current_user.id) +      followed = User.get_by_id(followed.id)        refute User.ap_followers(followed) in current_user.following @@ -764,7 +764,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/friendships/destroy.json", %{user_id: followed.id}) -      current_user = Repo.get(User, current_user.id) +      current_user = User.get_by_id(current_user.id)        assert current_user.following == [current_user.ap_id]        assert json_response(conn, 200) == @@ -788,7 +788,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/blocks/create.json", %{user_id: blocked.id}) -      current_user = Repo.get(User, current_user.id) +      current_user = User.get_by_id(current_user.id)        assert User.blocks?(current_user, blocked)        assert json_response(conn, 200) == @@ -815,7 +815,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/blocks/destroy.json", %{user_id: blocked.id}) -      current_user = Repo.get(User, current_user.id) +      current_user = User.get_by_id(current_user.id)        assert current_user.info.blocks == []        assert json_response(conn, 200) == @@ -846,7 +846,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post("/api/qvitter/update_avatar.json", %{img: avatar_image}) -      current_user = Repo.get(User, current_user.id) +      current_user = User.get_by_id(current_user.id)        assert is_map(current_user.avatar)        assert json_response(conn, 200) == @@ -954,8 +954,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post(request_path) -      activity = Repo.get(Activity, note_activity.id) -      activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"]) +      activity = Activity.get_by_id(note_activity.id) +      activity_user = User.get_by_ap_id(note_activity.data["actor"])        assert json_response(response, 200) ==                 ActivityView.render("activity.json", %{ @@ -992,8 +992,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          |> with_credentials(current_user.nickname, "test")          |> post(request_path) -      activity = Repo.get(Activity, note_activity.id) -      activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"]) +      activity = Activity.get_by_id(note_activity.id) +      activity_user = User.get_by_ap_id(note_activity.data["actor"])        assert json_response(response, 200) ==                 ActivityView.render("activity.json", %{ @@ -1021,7 +1021,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do        user = json_response(conn, 200) -      fetched_user = Repo.get_by(User, nickname: "lain") +      fetched_user = User.get_by_nickname("lain")        assert user == UserView.render("show.json", %{user: fetched_user})      end @@ -1109,7 +1109,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do      test "it confirms the user account", %{conn: conn, user: user} do        get(conn, "/api/account/confirm_email/#{user.id}/#{user.info.confirmation_token}") -      user = Repo.get(User, user.id) +      user = User.get_by_id(user.id)        refute user.info.confirmation_pending        refute user.info.confirmation_token @@ -1727,7 +1727,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do          })        assert json_response(conn, 200) == %{"status" => "success"} -      fetched_user = Repo.get(User, current_user.id) +      fetched_user = User.get_by_id(current_user.id)        assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true      end    end @@ -1768,8 +1768,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false @@ -1808,8 +1808,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false @@ -1831,8 +1831,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do        {:ok, _activity} = ActivityPub.follow(other_user, user) -      user = Repo.get(User, user.id) -      other_user = Repo.get(User, other_user.id) +      user = User.get_by_id(user.id) +      other_user = User.get_by_id(other_user.id)        assert User.following?(other_user, user) == false diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index b823bfd68..6c00244de 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -275,7 +275,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:ok, user} = TwitterAPI.register_user(data) -    fetched_user = Repo.get_by(User, nickname: "lain") +    fetched_user = User.get_by_nickname("lain")      assert UserView.render("show.json", %{user: user}) ==               UserView.render("show.json", %{user: fetched_user}) @@ -293,7 +293,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:ok, user} = TwitterAPI.register_user(data) -    fetched_user = Repo.get_by(User, nickname: "lain") +    fetched_user = User.get_by_nickname("lain")      assert UserView.render("show.json", %{user: user}) ==               UserView.render("show.json", %{user: fetched_user}) @@ -369,7 +369,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:ok, user} = TwitterAPI.register_user(data) -    fetched_user = Repo.get_by(User, nickname: "vinny") +    fetched_user = User.get_by_nickname("vinny")      token = Repo.get_by(UserInviteToken, token: token.token)      assert token.used == true @@ -393,7 +393,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:error, msg} = TwitterAPI.register_user(data)      assert msg == "Invalid token" -    refute Repo.get_by(User, nickname: "GrimReaper") +    refute User.get_by_nickname("GrimReaper")    end    @moduletag skip: "needs 'registrations_open: false' in config" @@ -414,7 +414,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:error, msg} = TwitterAPI.register_user(data)      assert msg == "Expired token" -    refute Repo.get_by(User, nickname: "GrimReaper") +    refute User.get_by_nickname("GrimReaper")    end    test "it returns the error on registration problems" do @@ -429,7 +429,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do      {:error, error_object} = TwitterAPI.register_user(data)      assert is_binary(error_object[:error]) -    refute Repo.get_by(User, nickname: "lain") +    refute User.get_by_nickname("lain")    end    test "it assigns an integer conversation_id" do diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 832fdc096..410f20f87 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -6,6 +6,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do    alias Pleroma.Web.CommonAPI    import Pleroma.Factory +  setup do +    Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) +    :ok +  end +    describe "POST /api/pleroma/follow_import" do      test "it returns HTTP 200", %{conn: conn} do        user1 = insert(:user) @@ -164,4 +169,47 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do        assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()      end    end + +  describe "/api/pleroma/emoji" do +    test "returns json with custom emoji with tags", %{conn: conn} do +      [emoji | _body] = +        conn +        |> get("/api/pleroma/emoji") +        |> json_response(200) + +      [key] = Map.keys(emoji) + +      %{ +        ^key => %{ +          "image_url" => url, +          "tags" => tags +        } +      } = emoji + +      assert is_binary(url) +      assert is_list(tags) +    end +  end + +  describe "GET /ostatus_subscribe?acct=...." do +    test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do +      conn = +        get( +          conn, +          "/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009" +        ) + +      assert redirected_to(conn) =~ "/notice/" +    end + +    test "show follow account page if the `acct` is a account link", %{conn: conn} do +      response = +        get( +          conn, +          "/ostatus_subscribe?acct=https://mastodon.social/users/emelie" +        ) + +      assert html_response(response, 200) =~ "Log in to follow" +    end +  end  end diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs index a1776b3e6..ee9a0c834 100644 --- a/test/web/twitter_api/views/activity_view_test.exs +++ b/test/web/twitter_api/views/activity_view_test.exs @@ -281,7 +281,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do      convo_id = Utils.context_to_conversation_id(activity.data["object"]["context"]) -    activity = Repo.get(Activity, activity.id) +    activity = Activity.get_by_id(activity.id)      result = ActivityView.render("activity.json", activity: announce) diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index 4e7f94795..0feaf4b64 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -292,7 +292,7 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do        }      } -    blocker = Repo.get(User, blocker.id) +    blocker = User.get_by_id(blocker.id)      assert represented == UserView.render("show.json", %{user: user, for: blocker})    end | 
