Browse Source

Merge remote-tracking branch 'origin/develop' into feature/bbs

tags/v1.1.4
lain 5 years ago
parent
commit
6ad8ddfd29
100 changed files with 5341 additions and 1126 deletions
  1. +3
    -1
      .gitignore
  2. +35
    -36
      .gitlab-ci.yml
  3. +0
    -0
      AGPL-3
  4. +403
    -0
      CC-BY-NC-ND-4.0
  5. +427
    -0
      CC-BY-SA-4.0
  6. +113
    -0
      CHANGELOG.md
  7. +41
    -0
      COPYING
  8. +6
    -4
      README.md
  9. +98
    -8
      config/config.exs
  10. +1
    -2
      config/dev.exs
  11. +2
    -3
      config/emoji.txt
  12. +10
    -1
      config/test.exs
  13. +4
    -2
      docs/admin/backup.md
  14. +5
    -5
      docs/admin/updating.md
  15. +76
    -3
      docs/api/admin_api.md
  16. +38
    -2
      docs/api/differences_in_mastoapi_responses.md
  17. +160
    -6
      docs/api/pleroma_api.md
  18. +22
    -0
      docs/api/prometheus.md
  19. +178
    -22
      docs/config.md
  20. +57
    -7
      docs/config/custom_emoji.md
  21. +0
    -7
      docs/config/howto_change_ip_and_port.md
  22. +1
    -2
      docs/installation/arch_linux_en.md
  23. +296
    -0
      docs/installation/gentoo_en.md
  24. +6
    -6
      docs/introduction.md
  25. +45
    -0
      installation/download-mastofe-build.sh
  26. +1
    -1
      installation/pleroma.nginx
  27. +21
    -0
      installation/pleroma.supervisord
  28. +60
    -0
      lib/healthcheck.ex
  29. +25
    -0
      lib/mix/tasks/benchmark.ex
  30. +51
    -0
      lib/mix/tasks/pleroma/database.ex
  31. +293
    -0
      lib/mix/tasks/pleroma/emoji.ex
  32. +49
    -2
      lib/mix/tasks/pleroma/instance.ex
  33. +2
    -0
      lib/mix/tasks/pleroma/robots_txt.eex
  34. +1
    -1
      lib/mix/tasks/pleroma/sample_config.eex
  35. +102
    -18
      lib/mix/tasks/pleroma/user.ex
  36. +1
    -1
      lib/pleroma/PasswordResetToken.ex
  37. +23
    -54
      lib/pleroma/activity.ex
  38. +23
    -3
      lib/pleroma/application.ex
  39. +60
    -0
      lib/pleroma/bookmark.ex
  40. +4
    -0
      lib/pleroma/config.ex
  41. +6
    -3
      lib/pleroma/emails/admin_email.ex
  42. +1
    -1
      lib/pleroma/emails/mailer.ex
  43. +3
    -2
      lib/pleroma/emails/user_email.ex
  44. +128
    -80
      lib/pleroma/emoji.ex
  45. +1
    -1
      lib/pleroma/flake_id.ex
  46. +49
    -16
      lib/pleroma/formatter.ex
  47. +4
    -4
      lib/pleroma/gopher/server.ex
  48. +58
    -13
      lib/pleroma/html.ex
  49. +1
    -1
      lib/pleroma/list.ex
  50. +64
    -7
      lib/pleroma/notification.ex
  51. +17
    -8
      lib/pleroma/object.ex
  52. +61
    -0
      lib/pleroma/object/containment.ex
  53. +75
    -0
      lib/pleroma/object/fetcher.ex
  54. +6
    -0
      lib/pleroma/pagination.ex
  55. +1
    -1
      lib/pleroma/plugs/http_security_plug.ex
  56. +10
    -0
      lib/pleroma/plugs/oauth_plug.ex
  57. +2
    -20
      lib/pleroma/plugs/user_fetcher_plug.ex
  58. +57
    -0
      lib/pleroma/registration.ex
  59. +4
    -0
      lib/pleroma/repo.ex
  60. +161
    -0
      lib/pleroma/scheduled_activity.ex
  61. +58
    -0
      lib/pleroma/scheduled_activity_worker.ex
  62. +144
    -83
      lib/pleroma/user.ex
  63. +37
    -8
      lib/pleroma/user/info.ex
  64. +96
    -17
      lib/pleroma/user_invite_token.ex
  65. +79
    -112
      lib/pleroma/web/activity_pub/activity_pub.ex
  66. +6
    -4
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  67. +2
    -2
      lib/pleroma/web/activity_pub/relay.ex
  68. +103
    -113
      lib/pleroma/web/activity_pub/transmogrifier.ex
  69. +57
    -22
      lib/pleroma/web/activity_pub/utils.ex
  70. +6
    -1
      lib/pleroma/web/activity_pub/views/user_view.ex
  71. +14
    -9
      lib/pleroma/web/activity_pub/visibility.ex
  72. +58
    -14
      lib/pleroma/web/admin_api/admin_api_controller.ex
  73. +18
    -0
      lib/pleroma/web/admin_api/views/account_view.ex
  74. +48
    -2
      lib/pleroma/web/auth/authenticator.ex
  75. +23
    -29
      lib/pleroma/web/auth/ldap_authenticator.ex
  76. +70
    -10
      lib/pleroma/web/auth/pleroma_authenticator.ex
  77. +1
    -1
      lib/pleroma/web/channels/user_socket.ex
  78. +32
    -17
      lib/pleroma/web/common_api/common_api.ex
  79. +51
    -12
      lib/pleroma/web/common_api/utils.ex
  80. +5
    -0
      lib/pleroma/web/controller_helper.ex
  81. +29
    -4
      lib/pleroma/web/endpoint.ex
  82. +3
    -2
      lib/pleroma/web/federator/federator.ex
  83. +34
    -1
      lib/pleroma/web/mastodon_api/mastodon_api.ex
  84. +323
    -94
      lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
  85. +44
    -5
      lib/pleroma/web/mastodon_api/views/account_view.ex
  86. +57
    -0
      lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
  87. +97
    -45
      lib/pleroma/web/mastodon_api/views/status_view.ex
  88. +1
    -1
      lib/pleroma/web/mastodon_api/websocket_handler.ex
  89. +31
    -19
      lib/pleroma/web/media_proxy/media_proxy.ex
  90. +13
    -0
      lib/pleroma/web/metadata/rel_me.ex
  91. +1
    -1
      lib/pleroma/web/metadata/utils.ex
  92. +16
    -3
      lib/pleroma/web/oauth/fallback_controller.ex
  93. +280
    -60
      lib/pleroma/web/oauth/oauth_controller.ex
  94. +1
    -1
      lib/pleroma/web/oauth/token.ex
  95. +18
    -24
      lib/pleroma/web/ostatus/activity_representer.ex
  96. +3
    -2
      lib/pleroma/web/ostatus/handlers/note_handler.ex
  97. +1
    -1
      lib/pleroma/web/ostatus/ostatus.ex
  98. +17
    -14
      lib/pleroma/web/push/impl.ex
  99. +10
    -38
      lib/pleroma/web/push/push.ex
  100. +2
    -1
      lib/pleroma/web/rel_me.ex

+ 3
- 1
.gitignore View File

@@ -3,7 +3,6 @@
/db
/deps
/*.ez
/uploads
/test/uploads
/.elixir_ls
/test/fixtures/test_tmp.txt
@@ -36,3 +35,6 @@ erl_crash.dump

# Editor config
/.vscode/

# Prevent committing docs files
/priv/static/doc/*

+ 35
- 36
.gitlab-ci.yml View File

@@ -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,61 @@ 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 deps.get
- 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
@@ -79,4 +78,4 @@ docs_deploy:
- echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}"
- rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}"

LICENSE → AGPL-3 View File


+ 403
- 0
CC-BY-NC-ND-4.0 View File

@@ -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.


+ 427
- 0
CC-BY-SA-4.0 View File

@@ -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.

+ 113
- 0
CHANGELOG.md View File

@@ -0,0 +1,113 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]
### Added
- LDAP authentication
- External OAuth provider authentication
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Federation: Support for reports
- Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option
- Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint
- Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- ActivityPub C2S: OAuth endpoints
- Metadata RelMe provider
- Emoji packs and emoji pack manager

### Changed
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
- Enforcement of OAuth scopes
- Add multiple use/time expiring invite token
- Restyled OAuth pages to fit with Pleroma's default theme
- Link/mention/hashtag detection is now handled by [auto_linker](https://git.pleroma.social/pleroma/auto_linker)
- NodeInfo: Return `safe_dm_mentions` feature flag
- Federation: Expand the audience of delete activities to all recipients of the deleted object
- Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity
- Mastodon API: Add `pleroma.conversation_id`, `pleroma.in_reply_to_account_acct` fields to the Status entity
- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending`, `pleroma.hide_followers`, `pleroma.hide_follows`, `pleroma.hide_favorites` fields to the User entity
- Mastodon API: Add `pleroma.show_role`, `pleroma.no_rich_text` fields to the Source subentity
- Mastodon API: Add support for updating `no_rich_text`, `hide_followers`, `hide_follows`, `hide_favorites`, `show_role` in `PATCH /api/v1/update_credentials`
- Mastodon API: Add `pleroma.is_seen` to the Notification entity
- Mastodon API: Add `pleroma.local` to the Status entity
- Mastodon API: Add `preview` parameter to `POST /api/v1/statuses`
- Mastodon API: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity
- Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.

### Fixed
- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
- Followers counter not being updated when a follower is blocked
- Deactivated users being able to request an access token
- Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak
- proper Twitter Card generation instead of a dummy
- NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox
- Federation: Handling of objects without `summary` property
- Federation: Add a language tag to activities as required by ActivityStreams 2.0
- Federation: Do not federate avatar/banner if set to default allowing other servers/clients to use their defaults
- Federation: Cope with missing or explicitly nulled address lists
- Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection
- Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics
- Federation: Follow requests from remote users who have been blocked will be automatically rejected if appropriate
- MediaProxy: Parse name from content disposition headers even for non-whitelisted types
- MediaProxy: S3 link encoding
- Rich Media: Reject any data which cannot be explicitly encoded into JSON
- Pleroma API: Importing follows from Mastodon 2.8+
- Twitter API: Exposing default scope, `no_rich_text` of the user to anyone
- Twitter API: Returning the `role` object in user entity despite `show_role = false`
- Mastodon API: `/api/v1/favourites` serving only public activities
- Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies
- Mastodon API: Streaming API broadcasting wrong activity id
- Mastodon API: 500 errors when requesting a card for a private conversation
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone

## [0.9.9999] - 2019-04-05
### Security
- Mastodon API: Fix content warnings skipping HTML sanitization

## [0.9.999] - 2019-03-13
Frontend changes only.
### Added
- Added floating action button for posting status on mobile
### Changed
- Changed user-settings icon to a pencil
### Fixed
- Keyboard shortcuts activating when typing a message
- Gaps when scrolling down on a timeline after showing new

## [0.9.99] - 2019-03-08
### Changed
- Update the frontend to the 0.9.99 tag
### Fixed
- Sign the date header in federation to fix Mastodon federation.

## [0.9.9] - 2019-02-22
This is our first stable release.

+ 41
- 0
COPYING View File

@@ -0,0 +1,41 @@
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

+ 6
- 4
README.md View File

@@ -1,14 +1,16 @@
# Pleroma

**Note**: This readme as well as complete documentation is also available at <https://docs-develop.pleroma.social>

## About Pleroma

Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.

Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.

For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md).
For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/).

- [Client Applications for Pleroma](docs/Clients.md)
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)

No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.

@@ -28,7 +30,7 @@ While we don’t provide docker files, other people have written very good ones.

* Run `mix deps.get` to install elixir dependencies.
* Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step.
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [`docs/config.md`](docs/config.md) in the repository, or at the "Configuration" page on <https://docs.pleroma.social>
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [`docs/config.md`](docs/config.md) in the repository, or at the "Configuration" page on <https://docs-develop.pleroma.social/config.html>
* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
* You can check if your instance is configured correctly by running it with `mix phx.server` and checking the instance info endpoint at `/api/v1/instance`. If it shows your uri, name and email correctly, you are configured correctly. If it shows something like `localhost:4000`, your configuration is probably wrong, unless you are running a local development setup.
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: <https://letsencrypt.org/>. The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically.
@@ -66,7 +68,7 @@ This is useful for running Pleroma inside Tor or I2P.

## Customization and contribution

The [Pleroma Wiki](https://git.pleroma.social/pleroma/pleroma/wikis/home) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
The [Pleroma Documentation](https://docs-develop.pleroma.social/readme.html) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.

## Troubleshooting



+ 98
- 8
config/config.exs View File

@@ -1,3 +1,41 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
@@ -8,6 +46,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 +96,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`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"

config :pleroma, :uri_schemes,
valid_schemes: [
@@ -87,6 +135,7 @@ websocket_config = [

# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
dispatch: [
@@ -105,7 +154,10 @@ config :pleroma, Pleroma.Web.Endpoint,
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
secure_cookie_flag: true
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]

# Configures Elixir's Logger
config :logger, :console,
@@ -118,6 +170,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"],
@@ -144,6 +201,7 @@ config :pleroma, :http,
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
remote_limit: 100_000,
@@ -163,9 +221,9 @@ config :pleroma, :instance,
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown"
"text/markdown",
"text/bbcode"
],
finmoji_enabled: true,
mrf_transparency: true,
autofollowed_nicknames: [],
max_pinned_statuses: 1,
@@ -173,7 +231,8 @@ config :pleroma, :instance,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false
safe_dm_mentions: false,
healthcheck: false

config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
@@ -268,7 +327,8 @@ config :pleroma, :media_proxy,
follow_redirect: true,
pool: :media
]
]
],
whitelist: []

config :pleroma, :chat, enabled: true

@@ -279,7 +339,9 @@ config :pleroma, :gopher,
ip: {0, 0, 0, 0},
port: 9999

config :pleroma, Pleroma.Web.Metadata, providers: [], unfurl_nsfw: false
config :pleroma, Pleroma.Web.Metadata,
providers: [Pleroma.Web.Metadata.Providers.RelMe],
unfurl_nsfw: false

config :pleroma, :suggestions,
enabled: false,
@@ -351,7 +413,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,
@@ -381,6 +446,31 @@ config :pleroma, :ldap,
config :esshd,
enabled: false

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.Emails.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"

+ 1
- 2
config/dev.exs View File

@@ -12,14 +12,13 @@ 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,
watchers: [],
secure_cookie_flag: false

config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Local
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local

# ## SSL Support
#


+ 2
- 3
config/emoji.txt View File

@@ -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


+ 10
- 1
config/test.exs View File

@@ -21,7 +21,11 @@ config :pleroma, Pleroma.Upload, filters: [], link_name: false

config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"

config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Test
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test

config :pleroma, :instance,
email: "admin@example.com",
notify_email: "noreply@example.com"

# Configure your database
config :pleroma, Pleroma.Repo,
@@ -50,6 +54,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


+ 4
- 2
docs/admin/backup.md View File

@@ -1,4 +1,6 @@
# Backup your instance
# Backup/Restore your instance

## Backup

1. Stop the Pleroma service.
2. Go to the working directory of Pleroma (default is `/opt/pleroma`)
@@ -6,7 +8,7 @@
4. Copy `pleroma.pgdump`, `config/prod.secret.exs` and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too.
5. Restart the Pleroma service.

## Restore your instance
## Restore

1. Stop the Pleroma service.
2. Go to the working directory of Pleroma (default is `/opt/pleroma`)


+ 5
- 5
docs/admin/updating.md View File

@@ -1,9 +1,9 @@
# Updating your instance
1. Stop the Pleroma service.
2. Go to the working directory of Pleroma (default is `/opt/pleroma`)
3. Run `git pull`. This pulls the latest changes from upstream.
4. Run `mix deps.get`. This pulls in any new dependencies.
1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
2. Run `git pull`. This pulls the latest changes from upstream.
3. Run `mix deps.get`. This pulls in any new dependencies.
4. Stop the Pleroma service.
5. Run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
6. Restart the Pleroma service.
6. Start the Pleroma service.

[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.

+ 76
- 3
docs/api/admin_api.md View File

@@ -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
@@ -180,12 +200,65 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret

## `/api/pleroma/admin/invite_token`

### Get a account registeration invite token
### Get an account registration invite token

- Methods: `GET`
- Params: none
- Params:
- *optional* `invite` => [
- *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07")
]
- Response: invite token (base64 string)

## `/api/pleroma/admin/invites`

### Get a list of generated invites

- Methods: `GET`
- Params: none
- Response:

```JSON
{

"invites": [
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
},
...
]
}
```

## `/api/pleroma/admin/revoke_invite`

### Revoke invite by token

- Methods: `POST`
- Params:
- `token`
- Response:

```JSON
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)

}
```


## `/api/pleroma/admin/email_invite`

### Sends registration invite via email
@@ -193,7 +266,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `POST`
- Params:
- `email`
- `name`, optionnal
- `name`, optional

## `/api/pleroma/admin/password_reset`



+ 38
- 2
docs/api/differences_in_mastoapi_responses.md View File

@@ -20,6 +20,9 @@ Has these additional fields under the `pleroma` object:

- `local`: true if the post was made on the local instance.
- `conversation_id`: the ID of the conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`

## Attachments

@@ -35,12 +38,45 @@ Has these additional fields under the `pleroma` object:

- `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship
- `is_moderator`: boolean, true if user is a moderator
- `is_admin`: boolean, true if user is an admin
- `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled

### Source

Has these additional fields under the `pleroma` object:

- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API

## Account Search

Behavior has changed:

- `/api/v1/accounts/search`: Does not require authentication

## Notifications

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.
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.

## PATCH `/api/v1/update_credentials`

Additional parameters can be added to the JSON body/Form data:

- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API
- `hide_followers` - if true, user's followers will be hidden
- `hide_follows` - if true, user's follows will be hidden
- `hide_favorites` - if true, user's favorites timeline will be hidden
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity

+ 160
- 6
docs/api/pleroma_api.md View File

@@ -10,7 +10,29 @@ 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:
```json
{
"girlpower": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/girlpower-128.png"
},
"education": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/education-128.png"
},
"finnishlove": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/finnishlove-128.png"
}
}
```
* Note: Same data as Mastodon API’s `/api/v1/custom_emojis` but in a different format

## `/api/pleroma/follow_import`
@@ -27,14 +49,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."}`
@@ -52,10 +74,10 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* `confirm`
* `captcha_solution`: optional, contains provider-specific captcha solution,
* `captcha_token`: optional, contains provider-specific captcha token
* `token`: invite token required when the registerations aren't public.
* `token`: invite token required when the registrations aren't public.
* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```
```json
{
"background_image": null,
"cover_photo": "https://pleroma.soykaf.com/images/banner.png",
@@ -114,5 +136,137 @@ See [Admin-API](Admin-API.md)
* Method `POST`
* Authentication: required
* Params:
* `id`: notifications's id
* `id`: notification's id
* Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}`

## `/api/v1/pleroma/accounts/:id/subscribe`
### Subscribe to receive notifications for all statuses posted by a user
* Method `POST`
* Authentication: required
* Params:
* `id`: account id to subscribe to
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"following": true,
"followed_by": false,
"blocking": false,
"muting": false,
"muting_notifications": false,
"subscribing": true,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
"endorsed": false
}
```

## `/api/v1/pleroma/accounts/:id/unsubscribe`
### Unsubscribe to stop receiving notifications from user statuses
* Method `POST`
* Authentication: required
* Params:
* `id`: account id to unsubscribe from
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"following": true,
"followed_by": false,
"blocking": false,
"muting": false,
"muting_notifications": false,
"subscribing": false,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
"endorsed": false
}
```

## `/api/v1/pleroma/accounts/:id/favourites`
### Returns favorites timeline of any user
* Method `GET`
* Authentication: not required
* Params:
* `id`: the id of the account for whom to return results
* `limit`: optional, the number of records to retrieve
* `since_id`: optional, returns results that are more recent than the specified id
* `max_id`: optional, returns results that are older than the specified id
* Response: JSON, returns a list of Mastodon Status entities on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
[
{
"account": {
"id": "9hptFmUF3ztxYh3Svg",
"url": "https://pleroma.example.org/users/nick2",
"username": "nick2",
...
},
"application": {"name": "Web", "website": null},
"bookmarked": false,
"card": null,
"content": "This is :moominmamma: note 0",
"created_at": "2019-04-15T15:42:15.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 1,
"id": "9hptFmVJ02khbzYJaS",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {"text/plain": "This is :moominmamma: note 0"},
"conversation_id": 13679,
"local": true,
"spoiler_text": {"text/plain": "2hu"}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "2hu",
"tags": [{"name": "2hu", "url": "/tag/2hu"}],
"uri": "https://pleroma.example.org/objects/198ed2a1-7912-4482-b559-244a0369e984",
"url": "https://pleroma.example.org/notice/9hptFmVJ02khbzYJaS",
"visibility": "public"
}
]
```

## `/api/pleroma/notification_settings`
### Updates user notification settings
* Method `PUT`
* Authentication: required
* Params:
* `followers`: BOOLEAN field, receives notifications from followers
* `follows`: BOOLEAN field, receives notifications from people the user follows
* `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`

## `/api/pleroma/healthcheck`
### Healthcheck endpoint with additional system data.
* Method `GET`
* Authentication: not required
* Params: none
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
* Example response:
```json
{
"pool_size": 0, # database connection pool
"active": 0, # active processes
"idle": 0, # idle processes
"memory_used": 0.00, # Memory used
"healthy": true # Instance state
}
```

+ 22
- 0
docs/api/prometheus.md View File

@@ -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']
```

+ 178
- 22
docs/config.md View File

@@ -31,22 +31,22 @@ This filter replaces the filename (not the path) of an upload. For complete obfu

* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used.

## Pleroma.Mailer
## Pleroma.Emails.Mailer
* `adapter`: one of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters), or `Swoosh.Adapters.Local` for in-memory mailbox.
* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.

An example for Sendgrid adapter:

```exs
config :pleroma, Pleroma.Mailer,
```elixir
config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "YOUR_API_KEY"
```

An example for SMTP adapter:

```exs
config :pleroma, Pleroma.Mailer,
```elixir
config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.gmail.com",
username: "YOUR_USERNAME@gmail.com",
@@ -63,6 +63,7 @@ config :pleroma, Pleroma.Mailer,
## :instance
* `name`: The instance’s name
* `email`: Email used to reach an Administrator/Moderator of the instance
* `notify_email`: Email used for notifications.
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``
* `limit`: Posts character limit (CW/Subject included in the counter)
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
@@ -86,7 +87,6 @@ config :pleroma, Pleroma.Mailer,
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
* `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis.
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default.
* `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values:
@@ -103,12 +103,13 @@ config :pleroma, Pleroma.Mailer,
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`)
* `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`)
* `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.

## :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:
```
```elixir
config :logger,
backends: [{ExSyslogger, :ex_syslogger}]

@@ -117,7 +118,7 @@ config :logger, :ex_syslogger,
```

Another example, keeping console output and adding the pid to syslog output:
```
```elixir
config :logger,
backends: [:console, {ExSyslogger, :ex_syslogger}]

@@ -128,6 +129,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:
```elixir
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

@@ -137,14 +156,30 @@ Frontends can access these settings at `/api/pleroma/frontend_configurations`

To add your own configuration for PleromaFE, use it like this:

`config :pleroma, :frontend_configurations, pleroma_fe: %{redirectRootNoLogin: "/main/all", ...}`
```elixir
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
# ... see /priv/static/static/config.json for the available keys.
},
masto_fe: %{
showInstanceSpecificPanel: true
}
```

These settings **need to be complete**, they will override the defaults.

These settings need to be complete, they will override the defaults. See `priv/static/static/config.json` for the available keys.
NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below.

## :fe
__THIS IS DEPRECATED__

If you are using this method, please change it to the `frontend_configurations` method. Please set this option to false in your config like this: `config :pleroma, :fe, false`.
If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method.
Please **set this option to false** in your config like this:

```elixir
config :pleroma, :fe, false
```

This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.

@@ -186,6 +221,7 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy

## :gopher
* `enabled`: Enables the gopher interface
@@ -193,6 +229,46 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `port`: Port to bind to
* `dstport`: Port advertised in urls (optional, defaults to `port`)

## Pleroma.Web.Endpoint
`Phoenix` endpoint configuration, all configuration options can be viewed [here](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-dynamic-configuration), only common options are listed here
* `http` - a list containing http protocol configuration, all configuration options can be viewed [here](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html#module-options), only common options are listed here
- `ip` - a tuple consisting of 4 integers
- `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`
- `port`
- `path`
* `extra_cookie_attrs` - a list of `Key=Value` strings to be added as non-standard cookie attributes. Defaults to `["SameSite=Lax"]`. See the [SameSite article](https://www.owasp.org/index.php/SameSite) on OWASP for more info.



**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:
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "example.com", port: 2020, scheme: "https"],
http: [
# start copied from config.exs
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
# end copied from config.exs
],
port: 8080,
ip: {127, 0, 0, 1}
]
```

This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls starting with `https://example.com:2020`

## :activitypub
* ``accept_blocks``: Whether to accept incoming block activities from other instances
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
@@ -214,7 +290,7 @@ their ActivityPub ID.

An example:

```exs
```elixir
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```
@@ -243,7 +319,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end

Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example:

```exs
```elixir
config :pleroma, :admin_token, "somerandomtoken"
```

@@ -258,9 +334,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)
* `mailer` - Email sender, see [`Pleroma.Emails.Mailer`](#pleroma-emails-mailer)
* `transmogrifier` - Transmogrifier
* `web_push` - Web push notifications
* `scheduled_activities` - Scheduled activities, see [`Pleroma.ScheduledActivities`](#pleromascheduledactivity)

Example:

@@ -280,9 +360,10 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`.
* `max_retries`: The maximum number of times a federation job is retried

## Pleroma.Web.Metadata
* `providers`: a list of metadata providers to enable. Providers availible:
* `providers`: a list of metadata providers to enable. Providers available:
* Pleroma.Web.Metadata.Providers.OpenGraph
* Pleroma.Web.Metadata.Providers.TwitterCard
* Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the `<header>` as `<link rel=me>`
* `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews

## :rich_media
@@ -322,7 +403,7 @@ Configuration for the `auto_linker` library:

Example:

```exs
```elixir
config :auto_linker,
opts: [
scheme: true,
@@ -334,6 +415,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
@@ -352,11 +444,6 @@ 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

* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication

## BBS / SSH access

To enable simple command line interface accessible over ssh, add a setting like this to your configuration file:
@@ -374,3 +461,72 @@ config :esshd,
```

Feel free to adjust the priv_dir and port number. Then you will have to create the key for the keys (in the example `priv/ssh_keys`) and create the host keys with `ssh-keygen -N "" -b 2048 -t rsa -f ssh_host_rsa_key`. After restarting, you should be able to connect to your Pleroma instance with `ssh username@server -p $PORT`

## :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.

Note: make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you have this feature enabled. OAuth consumer mode will not work with `"SameSite=Strict"`

* 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:

```elixir
# 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: []]}
]
```

## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).

+ 57
- 7
docs/config/custom_emoji.md View File

@@ -1,18 +1,68 @@
# Custom Emoji

Before you add your own custom emoji, check if they are available in an existing pack.
See `Mix.Tasks.Pleroma.Emoji` for information about emoji packs.

To add custom emoji:
* Add the image file(s) to `priv/static/emoji/custom`
* In case of conflicts: add the desired shortcode with the path to `config/custom_emoji.txt`, comma-separated and one per line
* Force recompilation (``mix clean && mix compile``)
* Create the `STATIC-DIR/emoji/` directory if it doesn't exist
(`STATIC-DIR` is configurable, `instance/static/` by default)
* Create a directory with whatever name you want (custom is a good name to show the purpose of it).
This will create a local emoji pack.
* Put your `.png` emoji files in that directory. In case of conflicts, you can create an `emoji.txt`
file in that directory and specify a custom shortcode using the following format:
`shortcode, file-path, tag1, tag2, etc`. One emoji per line. Note that if you do so,
you'll have to list all other emojis in the pack too.
* Either restart pleroma or connect to the iex session pleroma's running and
run `Pleroma.Emoji.reload/0` in it.

Example:

image files (in `/priv/static/emoji/custom`): `happy.png` and `sad.png`
image files (in `instance/static/emoji/custom`): `happy.png` and `sad.png`

content of `config/custom_emoji.txt`:
content of `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.

Default file extentions and locations for emojis are set in `config.exs`. To use different locations or file-extentions, add the `shortcode_globs` to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it. Note that not all fediverse-software will show emojis with other file extentions:
```elixir
config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png", "/emoji/custom/**/*.gif"]
```

## Emoji tags (groups)

Default tags are set in `config.exs`. To set your own tags, copy the structure to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it.
```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`

+ 0
- 7
docs/config/howto_change_ip_and_port.md View File

@@ -1,7 +0,0 @@
# How to change the port or IP Pleroma listens to
To change the port or IP Pleroma listens to, head over to your generated config inside the Pleroma folder at config/prod.secret.exs and edit the following according to your needs.
```
config :pleroma, Pleroma.Web.Endpoint,
[...]
http: [ip: {127, 0, 0, 1}, port: 4000]
```

+ 1
- 2
docs/installation/arch_linux_en.md View File

@@ -7,7 +7,6 @@ This guide will assume that you have administrative rights, either as root or a

* `postgresql`
* `elixir`
* `erlang-unixodbc`
* `git`
* `base-devel`

@@ -27,7 +26,7 @@ sudo pacman -Syu
* Install some of the above mentioned programs:

```shell
sudo pacman -S git base-devel elixir erlang-unixodbc
sudo pacman -S git base-devel elixir
```

### Install PostgreSQL


+ 296
- 0
docs/installation/gentoo_en.md View File

@@ -0,0 +1,296 @@
# Installing on Gentoo GNU/Linux
## Installation

This guide will assume that you have administrative rights, either as root or a user with [sudo permissions](https://wiki.gentoo.org/wiki/Sudo). Lines that begin with `#` indicate that they should be run as the superuser. Lines using `$` should be run as the indicated user, e.g. `pleroma$` should be run as the `pleroma` user.

### Configuring your hostname (optional)

If you would like your prompt to permanently include your host/domain, change `/etc/conf.d/hostname` to your hostname. You can reboot or use the `hostname` command to make immediate changes.

### Your make.conf, package.use, and USE flags

The only specific USE flag you should need is the `uuid` flag for `dev-db/postgresql`. Add the following line to any new file in `/etc/portage/package.use`. If you would like a suggested name for the file, either `postgresql` or `pleroma` would do fine, depending on how you like to arrange your package.use flags.

```text
dev-db/postgresql uuid
```

You could opt to add `USE="uuid"` to `/etc/portage/make.conf` if you'd rather set this as a global USE flags, but this flags does unrelated things in other packages, so keep that in mind if you elect to do so.

Double check your compiler flags in `/etc/portage/make.conf`. If you require any special compilation flags or would like to set up remote builds, now is the time to do so. Be sure that your CFLAGS and MAKEOPTS make sense for the platform you are using. It is not recommended to use above `-O2` or risky optimization flags for a production server.

### Installing a cron daemon

Gentoo quite pointedly does not come with a cron daemon installed, and as such it is recommended you install one to automate certbot renewals and to allow other system administration tasks to be run automatically. Gentoo has [a whole wide world of cron options](https://wiki.gentoo.org/wiki/Cron) but if you just want A Cron That Works, `emerge --ask virtual/cron` will install the default cron implementation (probably cronie) which will work just fine. For the purpouses of this guide, we will be doing just that.

### Required ebuilds

* `dev-db/postgresql`
* `dev-lang/elixir`
* `dev-vcs/git`

#### Optional ebuilds used in this guide

* `www-servers/nginx` (preferred, example configs for other reverse proxies can be found in the repo)
* `app-crypt/certbot` (or any other ACME client for Let’s Encrypt certificates)
* `app-crypt/certbot-nginx` (nginx certbot plugin that allows use of the all-powerful `--nginx` flag on certbot)

### Prepare the system

* First ensure that you have the latest copy of the portage ebuilds if you have not synced them yet:

```shell
# emaint sync -a
```

* Emerge all required the required and suggested software in one go:

```shell
# emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx
```

If you would not like to install the optional packages, remove them from this line.

If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, strech a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do.

### Install PostgreSQL

[Gentoo Wiki article](https://wiki.gentoo.org/wiki/PostgreSQL) as well as [PostgreSQL QuickStart](https://wiki.gentoo.org/wiki/PostgreSQL/QuickStart) might be worth a quick glance, as the way Gentoo handles postgres is slightly unusual, with built in capability to have two different databases running for testing and live or whatever other purpouse. While it is still straightforward to install, it does mean that the version numbers used in this guide might change for future updates, so keep an eye out for the output you get from `emerge` to ensure you are using the correct ones.

* Install postgresql if you have not done so already:

```shell
# emerge --ask dev-db/postgresql
```

Ensure that `/etc/conf.d/postgresql-11` has the encoding you want (it defaults to UTF8 which is probably what you want) and make any adjustments to the data directory if you find it necessary. Be sure to adjust the number at the end depending on what version of postgres you actually installed.

* Initialize the database cluster

The output from emerging postgresql should give you a command for initializing the postgres database. The default slot should be indicated in this command, ensure that it matches the command below.

```shell
# emerge --config dev-db/postgresql:11
```

* Start postgres and enable the system service
```shell
# /etc/init.d/postgresql-11 start
# rc-update add postgresql-11 default
```
### A note on licenses, the AGPL, and deployment procedures

If you do not plan to make any modifications to your Pleroma instance, cloning directly from the main repo will get you what you need. However, if you plan on doing any contributions to upstream development, making changes or modifications to your instance, making custom themes, or want to play around--and let's be honest here, if you're using Gentoo that is most likely you--you will save yourself a lot of headache later if you take the time right now to fork the Pleroma repo and use that in the following section.

Not only does this make it much easier to deploy changes you make, as you can commit and pull from upstream and all that good stuff from the comfort of your local machine then simply `git pull` on your instance server when you're ready to deploy, it also ensures you are compliant with the Affero General Public Licence that Pleroma is licenced under, which stipulates that all network services provided with modified AGPL code must publish their changes on a publicly available internet service and for free. It also makes it much easier to ask for help from and provide help to your fellow Pleroma admins if your public repo always reflects what you are running because it is part of your deployment procedure.

### Install PleromaBE

* Add a new system user for the Pleroma service and set up default directories:

Remove `,wheel` if you do not want this user to be able to use `sudo`, however note that being able to `sudo` as the `pleroma` user will make finishing the insallation and common maintenence tasks somewhat easier:

```shell
# useradd -m -G users,wheel -s /bin/bash pleroma
```

Optional: If you are using sudo, review your sudo setup to ensure it works for you. The `/etc/sudoers` file has a lot of options and examples to help you, and [the Gentoo sudo guide](https://wiki.gentoo.org/wiki/Sudo) has more information. Finishing this installation will be somewhat easier if you have a way to sudo from the `pleroma` user, but it might be best to not allow that user to sudo during normal operation, and as such there will be a reminder at the end of this guide to double check if you would like to lock down the `pleroma` user after initial setup.

**Note**: To execute a single command as the Pleroma system user, use `sudo -Hu pleroma command`. You can also switch to a shell by using `sudo -Hu pleroma $SHELL`. If you don't have or want `sudo` or would like to use the system as the `pleroma` user for instance maintenance tasks, you can simply use `su - pleroma` to switch to the `pleroma` user.

* Git clone the PleromaBE repository and make the Pleroma user the owner of the directory:

It is highly recommended you use your own fork for the `https://path/to/repo` part below, however if you foolishly decide to forego using your own fork, the primary repo `https://git.pleroma.social/pleroma/pleroma` will work here.

```shell
pleroma$ cd ~
pleroma$ git clone https://path/to/repo
```

* Change to the new directory:

```shell
pleroma$ cd ~/pleroma
```

* Install the dependencies for Pleroma and answer with `yes` if it asks you to install `Hex`:

```shell
pleroma$ mix deps.get
```

* Generate the configuration:

```shell
pleroma$ mix pleroma.instance gen
```

* Answer with `yes` if it asks you to install `rebar3`.

* This part precompiles some parts of Pleroma, so it might take a few moments

* After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`.

* Spend some time with `generated_config.exs` to ensure that everything is in order. If you plan on using an S3-compatible service to store your local media, that can be done here. You will likely mostly be using `prod.secret.exs` for a production instance, however if you would like to set up a development environment, make a copy to `dev.secret.exs` and adjust settings as needed as well.

```shell
pleroma$ mv config/generated_config.exs config/prod.secret.exs
```

* The previous command creates also the file `config/setup_db.psql`, with which you can create the database. Ensure that it is using the correct database name on the `CREATE DATABASE` and the `\c` lines, then run the postgres script:

```shell
pleroma$ sudo -Hu postgres psql -f config/setup_db.psql
```

* Now run the database migration:

```shell
pleroma$ MIX_ENV=prod mix ecto.migrate
```

* Now you can start Pleroma already

```shell
pleroma$ MIX_ENV=prod mix phx.server
```

It probably won't work over the public internet quite yet, however, as we still need to set up a web servere to proxy to the pleroma application, as well as configure SSL.

### Finalize installation

Assuming you want to open your newly installed federated social network to, well, the federation, you should run nginx or some other webserver/proxy in front of Pleroma. It is also a good idea to set up Pleroma to run as a system service.

#### Nginx

* Install nginx, if not already done:

```shell
# emerge --ask www-servers/nginx
```

* Create directories for available and enabled sites:

```shell
# mkdir -p /etc/nginx/sites-{available,enabled}
```

* Append the following line at the end of the `http` block in `/etc/nginx/nginx.conf`:

```Nginx
include sites-enabled/*;
```

* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already:

```shell
# emerge --ask app-crypt/certbot app-crypt/certbot-nginx
```

and then set it up:

```shell
# mkdir -p /var/lib/letsencrypt/
# certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
```

If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). Often the answer to issues with certbot is to use the `--nginx` flag once you have nginx up and running.

If you are using any additional subdomains, such as for a media proxy, you can re-run the same command with the subdomain in question. When it comes time to renew later, you will not need to run multiple times for each domain, one renew will handle it.

---

* Copy the example nginx configuration and activate it:

```shell
# cp /home/pleroma/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/
# ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
```

* Take some time to ensure that your nginx config is correct

Replace all instances of `example.tld` with your instance's public URL. If for whatever reason you made changes to the port that your pleroma app runs on, be sure that is reflected in your configuration.

Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stongly advised to comment that line out so that OpenSSL will use its full capabilities, and it is also possible you are running OpenSSL 1.0.2 necessitating that you do this.

* Enable and start nginx:

```shell
# rc-update add nginx default
# /etc/init.d/nginx start
```

If you are using certbot, it is HIGHLY recommend you set up a cron job that renews your certificate, and that you install the suggested `certbot-nginx` plugin. If you don't do these things, you only have yourself to blame when your instance breaks suddenly because you forgot about it.

First, ensure that the command you will be installing into your crontab works.

```shell
# /usr/bin/certbot renew --nginx
```

Assuming not much time has passed since you got certbot working a few steps ago, you should get a message for all domains you installed certificates for saying `Cert not yet due for renewal`.

Now, run crontab as a superuser with `crontab -e` or `sudo crontab -e` as appropriate, and add the following line to your cron:

```cron
0 0 1 * * /usr/bin/certbot renew --nginx
```

This will run certbot on the first of the month at midnight. If you'd rather run more frequently, it's not a bad idea, feel free to go for it.

#### Other webserver/proxies

If you would like to use other webservers or proxies, there are example configurations for some popular alternatives in `/home/pleroma/pleroma/installation/`. You can, of course, check out [the Gentoo wiki](https://wiki.gentoo.org) for more information on installing and configuring said alternatives.

#### Create the uploads folder

Even if you are using S3, Pleroma needs someplace to store media posted on your instance. If you are using the `/home/pleroma/pleroma` root folder suggested by this guide, simply:

```shell
pleroma$ mkdir -p ~/pleroma/uploads
```

#### init.d service

* Copy example service file

```shell
# cp /home/pleroma/pleroma/installation/init.d/pleroma /etc/init.d/
```

* Be sure to take a look at this service file and make sure that all paths fit your installation

* Enable and start `pleroma`:

```shell
# rc-update add pleroma default
# /etc/init.d/pleroma start
```

#### Create your first user

If your instance is up and running, you can create your first user with administrative rights with the following task:

```shell
pleroma$ MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
```

#### Privilege cleanup

If you opted to allow sudo for the `pleroma` user but would like to remove the ability for greater security, now might be a good time to edit `/etc/sudoers` and/or change the groups the `pleroma` user belongs to. Be sure to restart the pleroma service afterwards to ensure it picks up on the changes.

#### Further reading

* [Admin tasks](Admin tasks)
* [Backup your instance](Backup-your-instance)
* [Configuration tips](General tips for customizing pleroma fe)
* [Hardening your instance](Hardening-your-instance)
* [How to activate mediaproxy](How-to-activate-mediaproxy)
* [Small Pleroma-FE customizations](Small customizations)
* [Updating your instance](Updating-your-instance)

## Questions

Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**.

+ 6
- 6
docs/introduction.md View File

@@ -1,11 +1,11 @@
# Introduction to Pleroma
**What is Pleroma?**
## What is Pleroma?
Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
One account on a instance is enough to talk to the entire fediverse!
**How can I use it?**
## How can I use it?

Pleroma instances are already widely deployed, a list can be found here:
http://distsn.org/pleroma-instances.html
@@ -14,14 +14,14 @@ If you don't feel like joining an existing instance, but instead prefer to deplo
Installation instructions can be found here:
[main Pleroma wiki](/)
**I got an account, now what?**
## I got an account, now what?
Great! Now you can explore the fediverse!
- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password.
(If you don't have one yet, click on Register) :slightly_smiling_face:

At this point you will have two columns in front of you.

***left column***
### Left column
- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers).
Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000).
If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile:
@@ -37,7 +37,7 @@ To post your status, simply press Submit.

- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses.

***right column***
### Right column
This is where the interesting stuff happens! :slight_smile:
Depending on the timeline you will see different statuses, but each status has a standard structure:
- Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status).
@@ -46,7 +46,7 @@ Depending on the timeline you will see different statuses, but each status has a
- The text of the status, including mentions. If you click on a mention, it will automatically open the profile page of that person.
- Four buttons (left to right): Reply, Repeat, Favorite, Delete.

**Mastodon interface**
## Mastodon interface
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile:
Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks:
For more information on the Mastodon interface, please look here:


+ 45
- 0
installation/download-mastofe-build.sh View File

@@ -0,0 +1,45 @@
#!/bin/sh
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
project_id="74"
project_branch="rebase/glitch-soc"
static_dir="instance/static"
# For bundling:
# project_branch="pleroma"
# static_dir="priv/static"

if [[ ! -d "${static_dir}" ]]
then
echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleroma’s repository?"
exit 1
fi

last_modified="$(curl -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)"

echo "branch:${project_branch}"
echo "Last-Modified:${last_modified}"

artifact="mastofe.zip"

if [[ -e mastofe.timestamp ]] && [[ "${last_modified}" != "" ]]
then
if [[ "$(cat mastofe.timestamp)" == "${last_modified}" ]]
then
echo "MastoFE is up-to-date, exiting…"
exit 0
fi
fi

curl -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit

# TODO: Update the emoji as well
rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit
unzip -q "${artifact}" || exit

cp public/assets/sw.js "${static_dir}/sw.js" || exit
cp -r public/packs "${static_dir}/packs" || exit

echo "${last_modified}" > mastofe.timestamp
rm -fr public
rm -i "${artifact}"

+ 1
- 1
installation/pleroma.nginx View File

@@ -37,7 +37,7 @@ server {
listen [::]:443 ssl http2;
ssl_session_timeout 5m;

ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;



+ 21
- 0
installation/pleroma.supervisord View File

@@ -0,0 +1,21 @@
; Assumes pleroma is installed in /home/pleroma/pleroma and running as the pleroma user
; Also assumes mix is in /usr/bin, this might differ on BSDs or niche Linux distros
; Logs into /home/pleroma/logs
[program:pleroma]
command=/usr/bin/mix phx.server
directory=/home/pleroma/pleroma
autostart=true
autorestart=true
user=pleroma
environment =
MIX_ENV=prod,
HOME=/home/pleroma,
USER=pleroma,
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/home/pleroma/bin:%(ENV_PATH)s",
PWD=/home/pleroma/pleroma
stdout_logfile=/home/pleroma/logs/stdout.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile=/home/pleroma/logs/stderr.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10

+ 60
- 0
lib/healthcheck.ex View File

@@ -0,0 +1,60 @@
defmodule Pleroma.Healthcheck do
@moduledoc """
Module collects metrics about app and assign healthy status.
"""
alias Pleroma.Healthcheck
alias Pleroma.Repo

defstruct pool_size: 0,
active: 0,
idle: 0,
memory_used: 0,
healthy: true

@type t :: %__MODULE__{
pool_size: non_neg_integer(),
active: non_neg_integer(),
idle: non_neg_integer(),
memory_used: number(),
healthy: boolean()
}

@spec system_info() :: t()
def system_info do
%Healthcheck{
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
}
|> assign_db_info()
|> check_health()
end

defp assign_db_info(healthcheck) do
database = Application.get_env(:pleroma, Repo)[:database]

query =
"select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"

result = Repo.query!(query)
pool_size = Application.get_env(:pleroma, Repo)[:pool_size]

db_info =
Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->
if state == "active" do
Map.put(states, :active, states.active + cnt)
else
Map.put(states, :idle, states.idle + cnt)
end
end)
|> Map.put(:pool_size, pool_size)

Map.merge(healthcheck, db_info)
end

@spec check_health(Healthcheck.t()) :: Healthcheck.t()
def check_health(%{pool_size: pool_size, active: active} = check)
when active >= pool_size do
%{check | healthy: false}
end

def check_health(check), do: check
end

+ 25
- 0
lib/mix/tasks/benchmark.ex View File

@@ -0,0 +1,25 @@
defmodule Mix.Tasks.Pleroma.Benchmark do
use Mix.Task
alias Mix.Tasks.Pleroma.Common

def run(["search"]) do
Common.start_pleroma()

Benchee.run(%{
"search" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe")
end
})
end

def run(["tag"]) do
Common.start_pleroma()

Benchee.run(%{
"tag" => fn ->
%{"type" => "Create", "tag" => "cofe"}
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
})
end
end

+ 51
- 0
lib/mix/tasks/pleroma/database.ex View File

@@ -0,0 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common
require Logger
use Mix.Task

@shortdoc "A collection of database related tasks"
@moduledoc """
A collection of database related tasks

## Replace embedded objects with their references

Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.

mix pleroma.database remove_embedded_objects

Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
"""
def run(["remove_embedded_objects" | args]) do
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)

Common.start_pleroma()
Logger.info("Removing embedded objects")

Pleroma.Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
)

if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")

Pleroma.Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
end

+ 293
- 0
lib/mix/tasks/pleroma/emoji.ex View File

@@ -0,0 +1,293 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task

@shortdoc "Manages emoji packs"
@moduledoc """
Manages emoji packs

## ls-packs

mix pleroma.emoji ls-packs [OPTION...]

Lists the emoji packs and metadata specified in the manifest.

### Options

- `-m, --manifest PATH/URL` - path to a custom manifest, it can
either be an URL starting with `http`, in that case the
manifest will be fetched from that address, or a local path

## get-packs

mix pleroma.emoji get-packs [OPTION...] PACKS

Fetches, verifies and installs the specified PACKS from the
manifest into the `STATIC-DIR/emoji/PACK-NAME`

### Options

- `-m, --manifest PATH/URL` - same as ls-packs

## gen-pack

mix pleroma.emoji gen-pack PACK-URL

Creates a new manifest entry and a file list from the specified
remote pack file. Currently, only .zip archives are recognized
as remote pack files and packs are therefore assumed to be zip
archives. This command is intended to run interactively and will
first ask you some basic questions about the pack, then download
the remote file and generate an SHA256 checksum for it, then
generate an emoji file list for you.

The manifest entry will either be written to a newly created
`index.json` file or appended to the existing one, *replacing*
the old pack with the same name if it was in the file previously.

The file list will be written to the file specified previously,
*replacing* that file. You _should_ check that the file list doesn't
contain anything you don't need in the pack, that is, anything that is
not an emoji (the whole pack is downloaded, but only emoji files
are extracted).
"""

@default_manifest Pleroma.Config.get!([:emoji, :default_manifest])

def run(["ls-packs" | args]) do
Application.ensure_all_started(:hackney)

{options, [], []} = parse_global_opts(args)

manifest =
fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)

Enum.each(manifest, fn {name, info} ->
to_print = [
{"Name", name},
{"Homepage", info["homepage"]},
{"Description", info["description"]},
{"License", info["license"]},
{"Source", info["src"]}
]

for {param, value} <- to_print do
IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
end

# A newline
IO.puts("")
end)
end

def run(["get-packs" | args]) do
Application.ensure_all_started(:hackney)

{options, pack_names, []} = parse_global_opts(args)

manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest

manifest = fetch_manifest(manifest_url)

for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name]
src_url = pack["src"]

IO.puts(
IO.ANSI.format([
"Downloading ",
:bright,
pack_name,
:normal,
" from ",
:underline,
src_url
])
)

binary_archive = Tesla.get!(src_url).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()

sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]

if archive_sha == String.upcase(pack["src_sha256"]) do
IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
else
IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))

raise "Bad SHA256 for #{pack_name}"
end

# The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"])

IO.puts(
IO.ANSI.format([
"Fetching the file list for ",
:bright,
pack_name,
:normal,
" from ",
:underline,
files_url
])
)

files = Tesla.get!(files_url).body |> Poison.decode!()

IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))

pack_path =
Path.join([
Pleroma.Config.get!([:instance, :static_dir]),
"emoji",
pack_name
])

files_to_unzip =
Enum.map(
files,
fn {_, f} -> to_charlist(f) end
)

{:ok, _} =
:zip.unzip(binary_archive,
cwd: pack_path,
file_list: files_to_unzip
)

IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))

emoji_txt_str =
Enum.map(
files,
fn {shortcode, path} ->
emojo_path = Path.join("/emoji/#{pack_name}", path)
"#{shortcode}, #{emojo_path}"
end
)
|> Enum.join("\n")

File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
else
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
end
end
end

def run(["gen-pack", src]) do
Application.ensure_all_started(:hackney)

proposed_name = Path.basename(src) |> Path.rootname()
name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
# If there's no name, use the default one
name = if String.length(name) > 0, do: name, else: proposed_name

license = String.trim(IO.gets("License: "))
homepage = String.trim(IO.gets("Homepage: "))
description = String.trim(IO.gets("Description: "))

proposed_files_name = "#{name}.json"
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name

default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")

exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
)

exts =
if String.length(exts) > 0 do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts
end

IO.puts("Downloading the pack and generating SHA256")

binary_archive = Tesla.get!(src).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()

IO.puts("SHA256 is #{archive_sha}")

pack_json = %{
name => %{
license: license,
homepage: homepage,
description: description,
src: src,
src_sha256: archive_sha,
files: files_name
}
}

tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")

{:ok, _} =
:zip.unzip(
binary_archive,
cwd: tmp_pack_dir
)

emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)

File.write!(files_name, Poison.encode!(emoji_map, pretty: true))

IO.puts("""

#{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed.
""")

if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Poison.decode!()

File.write!(
"index.json",
Poison.encode!(
Map.merge(
existing_data,
pack_json
),
pretty: true
)
)

IO.puts("index.json file has been update with the #{name} pack")
else
File.write!("index.json", Poison.encode!(pack_json, pretty: true))

IO.puts("index.json has been created with the #{name} pack")
end
end

defp fetch_manifest(from) do
Poison.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(from).body
else
File.read!(from)
end
)
end

defp parse_global_opts(args) do
OptionParser.parse(
args,
strict: [
manifest: :string
],
aliases: [
m: :manifest
]
)
end
end

+ 49
- 2
lib/mix/tasks/pleroma/instance.ex View File

@@ -24,10 +24,12 @@ defmodule Mix.Tasks.Pleroma.Instance do
- `--domain DOMAIN` - the domain of your instance
- `--instance-name INSTANCE_NAME` - the name of your instance
- `--admin-email ADMIN_EMAIL` - the email address of the instance admin
- `--notify-email NOTIFY_EMAIL` - email address for notifications
- `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use
- `--dbname DBNAME` - the name of the database to use
- `--dbuser DBUSER` - the user (aka role) to use for the database connection
- `--dbpass DBPASS` - the password to use for the database connection
- `--indexable Y/N` - Allow/disallow indexing site by search engines
"""

def run(["gen" | rest]) do
@@ -41,10 +43,12 @@ defmodule Mix.Tasks.Pleroma.Instance do
domain: :string,
instance_name: :string,
admin_email: :string,
notify_email: :string,
dbhost: :string,
dbname: :string,
dbuser: :string,
dbpass: :string
dbpass: :string,
indexable: :string
],
aliases: [
o: :output,
@@ -61,7 +65,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
will_overwrite = Enum.filter(paths, &File.exists?/1)
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)

unless not proceed? do
if proceed? do
[domain, port | _] =
String.split(
Common.get_option(
@@ -81,6 +85,22 @@ defmodule Mix.Tasks.Pleroma.Instance do

email = Common.get_option(options, :admin_email, "What is your admin email address?")

notify_email =
Common.get_option(
options,
:notify_email,
"What email address do you want to use for sending email notifications?",
email
)

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")

@@ -114,6 +134,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
domain: domain,
port: port,
email: email,
notify_email: notify_email,
name: name,
dbhost: dbhost,
dbname: dbname,
@@ -142,6 +163,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 +186,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

+ 2
- 0
lib/mix/tasks/pleroma/robots_txt.eex View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow: <%= if indexable, do: "", else: "/" %>

+ 1
- 1
lib/mix/tasks/pleroma/sample_config.eex View File

@@ -13,6 +13,7 @@ config :pleroma, Pleroma.Web.Endpoint,
config :pleroma, :instance,
name: "<%= name %>",
email: "<%= email %>",
notify_email: "<%= notify_email %>",
limit: 5000,
registrations_open: true,
dedupe_media: false
@@ -75,4 +76,3 @@ config :web_push_encryption, :vapid_details,
# storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
# object_url: "https://cdn-endpoint.provider.com/<container>"
#


+ 102
- 18
lib/mix/tasks/pleroma/user.ex View File

@@ -6,8 +6,8 @@ defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
alias Mix.Tasks.Pleroma.Common
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken

@shortdoc "Manages Pleroma users"
@moduledoc """
@@ -23,16 +23,32 @@ 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.

mix pleroma.user invite
mix pleroma.user invite [OPTION...]

Options:
- `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05")
- `--max_use NUMBER` - maximum numbers of token uses

## List generated invites

mix pleroma.user invites

## Revoke invite

mix pleroma.user revoke_invite TOKEN OR TOKEN_ID

## Delete the user's account.

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
@@ -110,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.User do

proceed? = assume_yes? or Mix.shell().yes?("Continue?")

unless not proceed? do
if proceed? do
Common.start_pleroma()

params = %{
@@ -146,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
Common.start_pleroma()

with %User{local: true} = user <- User.get_by_nickname(nickname) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user)
Mix.shell().info("User #{nickname} deleted.")
else
@@ -158,7 +174,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["toggle_activated", nickname]) do
Common.start_pleroma()

with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated)

Mix.shell().info(
@@ -173,7 +189,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["reset_password", nickname]) do
Common.start_pleroma()

with %User{local: true} = user <- User.get_by_nickname(nickname),
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
Mix.shell().info("Generated password reset token for #{user.nickname}")

@@ -195,14 +211,14 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["unsubscribe", nickname]) do
Common.start_pleroma()

with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
Mix.shell().info("Deactivating #{user.nickname}")
User.deactivate(user)

{:ok, friends} = User.get_friends(user)

Enum.each(friends, fn friend ->
user = Repo.get(User, user.id)
user = User.get_cached_by_id(user.id)

Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
@@ -210,7 +226,7 @@ defmodule Mix.Tasks.Pleroma.User do

:timer.sleep(500)

user = Repo.get(User, user.id)
user = User.get_cached_by_id(user.id)

if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
@@ -234,7 +250,7 @@ defmodule Mix.Tasks.Pleroma.User do
]
)

with %User{local: true} = user <- User.get_by_nickname(nickname) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :moderator) do
nil -> user
@@ -261,7 +277,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["tag", nickname | tags]) do
Common.start_pleroma()

with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)

Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@@ -274,7 +290,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["untag", nickname | tags]) do
Common.start_pleroma()

with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)

Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@@ -284,23 +300,91 @@ defmodule Mix.Tasks.Pleroma.User do
end
end

def run(["invite"]) do
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
strict: [
expires_at: :string,
max_use: :integer
]
)

options =
options
|> Keyword.update(:expires_at, {:ok, nil}, fn
nil -> {:ok, nil}
val -> Date.from_iso8601(val)
end)
|> Enum.into(%{})

Common.start_pleroma()

with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
Mix.shell().info("Generated user invite token")
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
Mix.shell().info(
"Generated user invite token " <> String.replace(invite.invite_type, "_", " ")
)

url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
token.token
invite.token
)

IO.puts(url)
else
error ->
Mix.shell().error("Could not create invite token: #{inspect(error)}")
end
end

def run(["invites"]) do
Common.start_pleroma()

Mix.shell().info("Invites list:")

UserInviteToken.list_invites()
|> Enum.each(fn invite ->
expire_info =
with expires_at when not is_nil(expires_at) <- invite.expires_at do
" | Expires at: #{Date.to_string(expires_at)}"
end

using_info =
with max_use when not is_nil(max_use) <- invite.max_use do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end

Mix.shell().info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
)
end)
end

def run(["revoke_invite", token]) do
Common.start_pleroma()

with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
Mix.shell().info("Invite for token #{token} was revoked.")
else
_ -> Mix.shell().error("No invite found with token #{token}")
end
end

def run(["delete_activities", nickname]) do
Common.start_pleroma()

with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.")
else
_ ->
Mix.shell().error("Could not create invite token.")
Mix.shell().error("No local user #{nickname}")
end
end



+ 1
- 1
lib/pleroma/PasswordResetToken.ex View File

@@ -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_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}


+ 23
- 54
lib/pleroma/activity.ex View File

@@ -10,6 +10,7 @@ defmodule Pleroma.Activity do
alias Pleroma.Object
alias Pleroma.Repo

import Ecto.Changeset
import Ecto.Query

@type t :: %__MODULE__{}
@@ -31,7 +32,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!
@@ -79,6 +80,13 @@ defmodule Pleroma.Activity do
)
end

def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end

def get_by_ap_id_with_object(ap_id) do
Repo.one(
from(
@@ -196,21 +204,27 @@ defmodule Pleroma.Activity do

def create_by_object_ap_id_with_object(_), do: nil

def get_create_by_object_ap_id_with_object(ap_id) do
def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
ap_id
|> create_by_object_ap_id_with_object()
|> Repo.one()
end

def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil
def get_create_by_object_ap_id_with_object(_), do: nil

def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
get_create_by_object_ap_id(ap_id)
defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
get_create_by_object_ap_id_with_object(ap_id)
end

def get_in_reply_to_activity(_), do: nil
defp get_in_reply_to_activity_from_object(_), do: nil

def get_in_reply_to_activity(%Activity{data: %{"object" => object}}) do
get_in_reply_to_activity_from_object(Object.normalize(object))
end

def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil

def delete_by_ap_id(id) when is_binary(id) do
by_object_ap_id(id)
@@ -218,6 +232,7 @@ defmodule Pleroma.Activity do
|> Repo.delete_all()
|> elem(1)
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
_ -> nil
end)
@@ -245,50 +260,4 @@ defmodule Pleroma.Activity do
|> where([s], s.actor == ^actor)
|> Repo.all()
end

def increase_replies_count(id) do
Activity
|> where(id: ^id)
|> update([a],
set: [
data:
fragment(
"""
jsonb_set(?, '{object, repliesCount}',
(coalesce((?->'object'->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
a.data,
a.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [activity]} -> activity
_ -> {:error, "Not found"}
end
end

def decrease_replies_count(id) do
Activity
|> where(id: ^id)
|> update([a],
set: [
data:
fragment(
"""
jsonb_set(?, '{object, repliesCount}',
(greatest(0, (?->'object'->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
a.data,
a.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [activity]} -> activity
_ -> {:error, "Not found"}
end
end
end

+ 23
- 3
lib/pleroma/application.ex View File

@@ -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


+ 60
- 0
lib/pleroma/bookmark.ex View File

@@ -0,0 +1,60 @@
defmodule Pleroma.Bookmark do
use Ecto.Schema

import Ecto.Changeset
import Ecto.Query

alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.FlakeId
alias Pleroma.Repo
alias Pleroma.User

@type t :: %__MODULE__{}

schema "bookmarks" do
belongs_to(:user, User, type: FlakeId)
belongs_to(:activity, Activity, type: FlakeId)

timestamps()
end

@spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do
attrs = %{
user_id: user_id,
activity_id: activity_id
}

%Bookmark{}
|> cast(attrs, [:user_id, :activity_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert()
end

@spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
Bookmark
|> where(user_id: ^user_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end

def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
|> where(activity_id: ^activity_id)
|> Repo.one()
end

@spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
where: b.activity_id == ^activity_id
)
|> Repo.one()
|> Repo.delete()
end
end

+ 4
- 0
lib/pleroma/config.ex View File

@@ -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

+ 6
- 3
lib/pleroma/emails/admin_email.ex View File

@@ -2,7 +2,7 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.AdminEmail do
defmodule Pleroma.Emails.AdminEmail do
@moduledoc "Admin emails"

import Swoosh.Email
@@ -11,7 +11,10 @@ defmodule Pleroma.AdminEmail do

defp instance_config, do: Pleroma.Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp instance_email, do: instance_config()[:email]

defp instance_notify_email do
Keyword.get(instance_config(), :notify_email, instance_config()[:email])
end

defp user_url(user) do
Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
@@ -59,7 +62,7 @@ defmodule Pleroma.AdminEmail do

new()
|> to({to.name, to.email})
|> from({instance_name(), instance_email()})
|> from({instance_name(), instance_notify_email()})
|> reply_to({reporter.name, reporter.email})
|> subject("#{instance_name()} Report")
|> html_body(html_body)


+ 1
- 1
lib/pleroma/emails/mailer.ex View File

@@ -2,7 +2,7 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Mailer do
defmodule Pleroma.Emails.Mailer do
use Swoosh.Mailer, otp_app: :pleroma

def deliver_async(email, config \\ []) do


+ 3
- 2
lib/pleroma/emails/user_email.ex View File

@@ -2,7 +2,7 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.UserEmail do
defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails"

import Swoosh.Email
@@ -15,7 +15,8 @@ defmodule Pleroma.UserEmail do
defp instance_name, do: instance_config()[:name]

defp sender do
{instance_name(), instance_config()[:email]}
email = Keyword.get(instance_config(), :notify_email, instance_config()[:email])
{instance_name(), email}
end

defp recipient(email, nil), do: email


+ 128
- 80
lib/pleroma/emoji.ex View File

@@ -6,15 +6,23 @@ defmodule Pleroma.Emoji do
@moduledoc """
The emojis are loaded from:

* the built-in Finmojis (if enabled in configuration),
* emoji packs in INSTANCE-DIR/emoji
* 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

require Logger

@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,91 +81,94 @@ defmodule Pleroma.Emoji do
end

defp load do
emoji_dir_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)

case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
nil

{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")

{:ok, packs} ->
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")

emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
)

true = :ets.insert(@ets, emojis)
end

# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []

emojis =
(load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++
load_from_file("config/emoji.txt") ++
(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)

:ok
end

@finmoji [
"a_trusted_friend",
"alandislands",
"association",
"auroraborealis",
"baby_in_a_box",
"bear",
"black_gold",
"christmasparty",
"crosscountryskiing",
"cupofcoffee",
"education",
"fashionista_finns",
"finnishlove",
"flag",
"forest",
"four_seasons_of_bbq",
"girlpower",
"handshake",
"happiness",
"headbanger",
"icebreaker",
"iceman",
"joulutorttu",
"kaamos",
"kalsarikannit_f",
"kalsarikannit_m",
"karjalanpiirakka",
"kicksled",
"kokko",
"lavatanssit",
"losthopes_f",
"losthopes_m",
"mattinykanen",
"meanwhileinfinland",
"moominmamma",
"nordicfamily",
"out_of_office",
"peacemaker",
"perkele",
"pesapallo",
"polarbear",
"pusa_hispida_saimensis",
"reindeer",
"sami",
"sauna_f",
"sauna_m",
"sauna_whisk",
"sisu",
"stuck",
"suomimainittu",
"superfood",
"swan",
"the_cap",
"the_conductor",
"the_king",
"the_voice",
"theoriginalsanta",
"tomoffinland",
"torillatavataan",
"unbreakable",
"waiting",
"white_nights",
"woollysocks"
]
defp load_finmoji(true) do
Enum.map(@finmoji, fn finmoji ->
{finmoji, "/finmoji/128px/#{finmoji}-128.png"}
end)
defp load_pack(pack_dir) do
pack_name = Path.basename(pack_dir)

emoji_txt = Path.join(pack_dir, "emoji.txt")

if File.exists?(emoji_txt) do
load_from_file(emoji_txt)
else
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
)

make_shortcode_to_file_map(pack_dir, [".png"])
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)

{shortcode, filename, [to_string(match_extra(@groups, filename))]}
end)
end
end

defp load_finmoji(_), do: []
def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end

def find_all_emoji(dir, exts) do
Enum.reduce(
File.ls!(dir),
[],
fn f, acc ->
filepath = Path.join(dir, f)

if File.dir?(filepath) do
acc ++ find_all_emoji(filepath, exts)
else
acc ++ [filepath]
end
end
)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end

defp load_from_file(file) do
if File.exists?(file) do
@@ -172,8 +183,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] ->
{name, file, [to_string(match_extra(@groups, file))]}

[name, file | tags] ->
{name, file, tags}

_ ->
nil
end
end)
|> Enum.to_list()
@@ -190,9 +207,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

+ 1
- 1
lib/pleroma/flake_id.ex View File

@@ -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


+ 49
- 16
lib/pleroma/formatter.ex View File

@@ -9,20 +9,31 @@ defmodule Pleroma.Formatter do
alias Pleroma.Web.MediaProxy

@safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength

@auto_linker_config hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4

def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
%User{} ->
# escape markdown characters with `\\`
# (we don't want something like @user__name to be parsed by markdown)
String.replace(mention, @markdown_characters_regex, "\\\\\\1")

_ ->
buffer
end
end

def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
ap_id = get_ap_id(user)
nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts)
nickname_text = get_nickname_text(nickname, opts)

link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
@@ -70,6 +81,25 @@ defmodule Pleroma.Formatter do
end
end

@doc """
Escapes a special characters in mention names.
"""
def mentions_escape(text, options \\ []) do
options =
Keyword.merge(options,
mention: true,
url: false,
mention_handler: &Pleroma.Formatter.escape_mention_handler/4
)

if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
else
AutoLinker.link(text, options)
end
end

def emojify(text) do
emojify(text, Emoji.get_all())
end
@@ -77,15 +107,13 @@ 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
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file)
}' />"
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
else
""
end
@@ -100,12 +128,23 @@ defmodule Pleroma.Formatter do

def demojify(text, nil), do: text

@doc "Outputs a list of the emoji-shortcodes in a 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: []

@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, file, _group}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end

def get_emoji_map(_), do: []

def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@@ -140,10 +179,4 @@ defmodule Pleroma.Formatter do

defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)

defp maybe_escape(str, %{mentions_escape: true}) do
String.replace(str, @markdown_characters_regex, "\\\\\\1")
end

defp maybe_escape(str, _), do: str
end

+ 4
- 4
lib/pleroma/gopher/server.ex View File

@@ -38,7 +38,7 @@ end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
@@ -76,14 +76,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"])

object = activity.data["object"]
object = Object.normalize(activity)
like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0

link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <>
info(HTML.strip_tags(String.replace(activity.data["object"]["content"], "<br>", "\r")))
info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
@@ -111,7 +111,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"])


+ 58
- 13
lib/pleroma/html.ex View File

@@ -28,25 +28,47 @@ 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 \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"

Cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
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,
&HtmlEntities.decode/1
)
end

def ensure_scrubbed_html(
content,
scrubbers
scrubbers,
fake,
callback
) do
{:commit, filter_tags(content, scrubbers)}
content =
content
|> filter_tags(scrubbers)
|> callback.()

if fake do
{:ignore, content}
else
{:commit, content}
end
end

defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
@@ -93,7 +115,14 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do

# links
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])

Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])

Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
@@ -102,12 +131,15 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
"noreferrer"
])

Meta.allow_tag_with_these_attributes("a", ["name", "title"])

# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])

# microformats
Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])

# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
@@ -119,6 +151,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"class",
"title",
"alt"
])
@@ -142,7 +175,14 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.strip_comments()

Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])

Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])

Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
@@ -151,6 +191,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"noreferrer"
])

Meta.allow_tag_with_these_attributes("a", ["name", "title"])

Meta.allow_tag_with_these_attributes("abbr", ["title"])

Meta.allow_tag_with_these_attributes("b", [])
@@ -164,11 +206,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])

Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])

@allow_inline_images Keyword.get(@markup, :allow_inline_images)

if @allow_inline_images do
@@ -178,6 +222,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"class",
"title",
"alt"
])


+ 1
- 1
lib/pleroma/list.ex View File

@@ -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_cached_by_id(account_id)

query =
from(


+ 64
- 7
lib/pleroma/notification.ex View File

@@ -98,6 +98,14 @@ defmodule Pleroma.Notification do
|> Repo.delete_all()
end

def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in Notification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end

def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)

@@ -122,13 +130,7 @@ defmodule Pleroma.Notification do

# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or
(activity.data["type"] == "Follow" and
Enum.any?(Notification.for_user(user), fn notif ->
notif.activity.data["type"] == "Follow" and
notif.activity.data["actor"] == activity.data["actor"]
end)) do
unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
@@ -148,10 +150,65 @@ defmodule Pleroma.Notification do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()

User.get_users_from_set(recipients, local_only)
end

def get_notified_from_activity(_, _local_only), do: []

def skip?(activity, user) do
[:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
|> Enum.any?(&skip?(&1, activity, user))
end

def skip?(:self, activity, user) do
activity.data["actor"] == user.ap_id
end

def skip?(:blocked, activity, user) do
actor = activity.data["actor"]
User.blocks?(user, %{ap_id: actor})
end

def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}),
do: true

def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}),
do: true

def skip?(:muted, activity, user) do
actor = activity.data["actor"]

User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity)
end

def skip?(
:followers,
activity,
%{info: %{notification_settings: %{"followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
User.following?(follower, user)
end

def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end

def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"]

Notification.for_user(user)
|> Enum.any?(fn
%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
_ -> false
end)
end

def skip?(_, _, _), do: false
end

+ 17
- 8
lib/pleroma/object.ex View File

@@ -7,6 +7,7 @@ defmodule Pleroma.Object do

alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
@@ -40,36 +41,44 @@ defmodule Pleroma.Object do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end

def normalize(_, fetch_remote \\ true)
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Activity{object: %Object{} = object}), do: object
def normalize(%Object{} = object, _), do: object
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
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)

Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")

normalize(ap_id)
normalize(ap_id, fetch_remote)
end

def normalize(%Activity{data: %{"object" => ap_id}}) do
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do
Logger.debug(
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
)

Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")

normalize(ap_id)
normalize(ap_id, fetch_remote)
end

# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}), do: normalize(ap_id)
def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(_), do: nil
def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote)
def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
def normalize(_, _), do: nil

# Owned objects can only be mutated by their owner
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),


+ 61
- 0
lib/pleroma/object/containment.ex View File

@@ -0,0 +1,61 @@
defmodule Pleroma.Object.Containment do
@moduledoc """
# Object Containment

This module contains some useful functions for containing objects to specific
origins and determining those origins. They previously lived in the
ActivityPub `Transmogrifier` module.

Object containment is an important step in validating remote objects to prevent
spoofing, therefore removal of object containment functions is NOT recommended.
"""
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end

def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end

def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end

def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end

@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error

def contain_origin(id, %{"actor" => _actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))

if id_uri.host == actor_uri.host do
:ok
else
:error
end
end

def contain_origin_from_id(_id, %{"id" => nil}), do: :error

def contain_origin_from_id(id, %{"id" => other_id} = _params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)

if id_uri.host == other_uri.host do
:ok
else
:error
end
end
end

+ 75
- 0
lib/pleroma/object/fetcher.ex View File

@@ -0,0 +1,75 @@
defmodule Pleroma.Object.Fetcher do
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus

require Logger

@httpoison Application.get_env(:pleroma, :httpoison)

# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")

with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data, false),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Containment.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity, false)}
else
{:error, {:reject, nil}} ->
{:reject, nil}

object = %Object{} ->
{:ok, object}

_e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")

case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
end
end

def fetch_object_from_id!(id) do
with {:ok, object} <- fetch_object_from_id(id) do
object
else
_e ->
nil
end
end

def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching object #{id} via AP")

with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end
end

+ 6
- 0
lib/pleroma/pagination.ex View File

@@ -36,6 +36,12 @@ defmodule Pleroma.Pagination do
limit: :integer
}

params =
Enum.reduce(params, %{}, fn
{key, _value}, acc when is_atom(key) -> Map.drop(acc, [key])
{key, value}, acc -> Map.put(acc, key, value)
end)

changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end


+ 1
- 1
lib/pleroma/plugs/http_security_plug.ex View File

@@ -35,7 +35,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = String.replace(static_url, "http", "ws")
websocket_url = Pleroma.Web.Endpoint.websocket_url()

connect_src = "connect-src 'self' #{static_url} #{websocket_url}"



+ 10
- 0
lib/pleroma/plugs/oauth_plug.ex View File

@@ -16,6 +16,16 @@ defmodule Pleroma.Plugs.OAuthPlug do

def call(%{assigns: %{user: %User{}}} = conn, _), do: conn

def call(%{params: %{"access_token" => access_token}} = conn, _) do
with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ -> conn
end
end

def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn),
{:ok, user, token_record} <- fetch_user_and_token(token_str) do


+ 2
- 20
lib/pleroma/plugs/user_fetcher_plug.ex View File

@@ -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

+ 57
- 0
lib/pleroma/registration.ex View File

@@ -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

+ 4
- 0
lib/pleroma/repo.ex View File

@@ -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.


+ 161
- 0
lib/pleroma/scheduled_activity.ex View File

@@ -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

+ 58
- 0
lib/pleroma/scheduled_activity_worker.ex View File

@@ -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

+ 144
- 83
lib/pleroma/user.ex View File

@@ -10,9 +10,10 @@ defmodule Pleroma.User do

alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
@@ -52,9 +53,10 @@ defmodule Pleroma.User do
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
has_many(:bookmarks, Bookmark)
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]})
@@ -260,6 +269,7 @@ defmodule Pleroma.User do
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
@@ -270,8 +280,10 @@ defmodule Pleroma.User do
if user.info.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver_async()
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()

{:ok, :enqueued}
else
{:ok, :noop}
end
@@ -410,7 +422,7 @@ defmodule Pleroma.User do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with %User{} = followed <- get_or_fetch(followed_identifier),
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
@@ -442,10 +454,13 @@ defmodule Pleroma.User do
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"

get_by_nickname(nickname)
get_cached_by_nickname(nickname)
end

def set_cache(user) do
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}

def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
@@ -491,7 +506,15 @@ defmodule Pleroma.User do

def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)

Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname)

case user_result do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end

def get_cached_by_nickname_or_id(nickname_or_id) do
@@ -505,11 +528,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
@@ -528,18 +550,19 @@ defmodule Pleroma.User do

def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
user
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# TODO turn into job
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end

user
{:ok, user}
else
_e -> nil
_e -> {:error, "not found " <> nickname}
end
end
end
@@ -886,7 +909,7 @@ defmodule Pleroma.User do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with %User{} = blocked <- get_or_fetch(blocked_identifier),
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
@@ -923,6 +946,38 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end

def subscribe(subscriber, %{ap_id: ap_id}) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])

with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked

if blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
info_cng =
subscribed.info
|> User.Info.add_to_subscribers(subscriber.ap_id)

change(subscribed)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
end
end

def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
info_cng =
user.info
|> User.Info.remove_from_subscribers(unsubscriber.ap_id)

change(user)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
end

def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
@@ -933,10 +988,20 @@ defmodule Pleroma.User do
blocker
end

blocker =
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
blocker
else
blocker
end

if following?(blocked, blocker) do
unfollow(blocked, blocker)
end

{:ok, blocker} = update_follower_count(blocker)

info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
@@ -950,7 +1015,7 @@ defmodule Pleroma.User do

# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id))
block(blocker, get_cached_by_ap_id(ap_id))
end

def unblock(blocker, %{ap_id: ap_id}) do
@@ -979,12 +1044,21 @@ defmodule Pleroma.User do
end)
end

def subscribed_to?(user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
Enum.member?(target.info.subscribers, user.ap_id)
end
end

def muted_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))

def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

def subscribers(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))

def block_domain(user, domain) do
info_cng =
user.info
@@ -1082,34 +1156,41 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end

def update_notification_settings(%User{} = user, settings \\ %{}) do
info_changeset = User.Info.update_notification_settings(user.info, settings)

change(user)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end

def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)

# 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}
@@ -1128,41 +1209,41 @@ defmodule Pleroma.User do

case ap_try do
{:ok, user} ->
user
{:ok, user}

_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
{:ok, user} -> {:ok, user}
_ -> {:error, "Could not fetch by AP id"}
end
end
end

def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
user = get_cached_by_ap_id(ap_id)

if !is_nil(user) and !User.needs_update?(user) do
user
{:ok, user}
else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])

user = fetch_by_ap_id(ap_id)
resp = fetch_by_ap_id(ap_id)

if should_fetch_initial do
with %User{} = user do
with {:ok, %User{} = user} = resp do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
end

user
resp
end
end

def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

if user = get_by_ap_id(relay_uri) do
if user = get_cached_by_ap_id(relay_uri) do
user
else
changes =
@@ -1197,7 +1278,7 @@ defmodule Pleroma.User do
end

def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
@@ -1209,13 +1290,11 @@ defmodule Pleroma.User do
defp blank?(n), do: n

def insert_or_update_user(data) do
data =
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])

cs = User.remote_user_creation(data)

Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|> remote_user_creation()
|> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
|> set_cache()
end

def ap_enabled?(%User{local: true}), do: true
@@ -1231,8 +1310,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_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@@ -1242,8 +1321,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_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@@ -1251,18 +1330,15 @@ defmodule Pleroma.User do
end
end

def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
def parse_bio(nil, _user), do: ""
def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end

def parse_bio(bio, user) do
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
def parse_bio(_), do: ""

def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]

@@ -1272,9 +1348,10 @@ defmodule Pleroma.User do
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
|> Formatter.emojify(emoji)
end

def parse_bio(_, _), do: ""

def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
@@ -1282,7 +1359,7 @@ defmodule Pleroma.User do
end

def tag(nickname, tags) when is_binary(nickname),
do: tag(User.get_by_nickname(nickname), tags)
do: tag(get_by_nickname(nickname), tags)

def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
@@ -1294,7 +1371,7 @@ defmodule Pleroma.User do
end

def untag(nickname, tags) when is_binary(nickname),
do: untag(User.get_by_nickname(nickname), tags)
do: untag(get_by_nickname(nickname), tags)

def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
@@ -1308,22 +1385,6 @@ defmodule Pleroma.User do
updated_user
end

def bookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
update_bookmarks(user, bookmarks)
end

def unbookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks -- [status_id])
update_bookmarks(user, bookmarks)
end

def update_bookmarks(%User{} = user, bookmarks) do
user
|> change(%{bookmarks: bookmarks})
|> update_and_set_cache
end

defp normalize_tags(tags) do
[tags]
|> List.flatten()


+ 37
- 8
lib/pleroma/user/info.ex View File

@@ -22,6 +22,7 @@ defmodule Pleroma.User.Info do
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
@@ -37,8 +38,14 @@ defmodule Pleroma.User.Info do
field(:salmon, :string, default: nil)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
field(:emoji, {:array, :map}, default: [])

field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
)

# Found in the wild
# ap_id -> Where is this used?
@@ -57,6 +64,19 @@ defmodule Pleroma.User.Info do
|> validate_required([:deactivated])
end

def update_notification_settings(info, settings) do
notification_settings =
info.notification_settings
|> Map.merge(settings)
|> Map.take(["remote", "local", "followers", "follows"])

params = %{notification_settings: notification_settings}

info
|> cast(params, [:notification_settings])
|> validate_required([:notification_settings])
end

def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
@@ -93,6 +113,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:blocks])
end

def set_subscribers(info, subscribers) do
params = %{subscribers: subscribers}

info
|> cast(params, [:subscribers])
|> validate_required([:subscribers])
end

def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
@@ -109,6 +137,14 @@ defmodule Pleroma.User.Info do
set_blocks(info, List.delete(info.blocks, blocked))
end

def add_to_subscribers(info, subscribed) do
set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
end

def remove_from_subscribers(info, subscribed) do
set_subscribers(info, List.delete(info.subscribers, subscribed))
end

def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}

@@ -168,6 +204,7 @@ defmodule Pleroma.User.Info do
:banner,
:hide_follows,
:hide_followers,
:hide_favorites,
:background,
:show_role
])
@@ -191,14 +228,6 @@ defmodule Pleroma.User.Info do
cast(info, params, [:confirmation_pending, :confirmation_token])
end

def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end

def mastodon_settings_update(info, settings) do
params = %{settings: settings}



+ 96
- 17
lib/pleroma/user_invite_token.ex View File

@@ -6,40 +6,119 @@ defmodule Pleroma.UserInviteToken do
use Ecto.Schema

import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.UserInviteToken

@type t :: %__MODULE__{}
@type token :: String.t()

schema "user_invite_tokens" do
field(:token, :string)
field(:used, :boolean, default: false)
field(:max_use, :integer)
field(:expires_at, :date)
field(:uses, :integer, default: 0)
field(:invite_type, :string)

timestamps()
end

def create_token do
@spec create_invite(map()) :: UserInviteToken.t()
def create_invite(params \\ %{}) do
%UserInviteToken{}
|> cast(params, [:max_use, :expires_at])
|> add_token()
|> assign_type()
|> Repo.insert()
end

defp add_token(changeset) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
put_change(changeset, :token, token)
end

token = %UserInviteToken{
used: false,
token: token
}
defp assign_type(%{changes: %{max_use: _max_use, expires_at: _expires_at}} = changeset) do
put_change(changeset, :invite_type, "reusable_date_limited")
end

defp assign_type(%{changes: %{expires_at: _expires_at}} = changeset) do
put_change(changeset, :invite_type, "date_limited")
end

defp assign_type(%{changes: %{max_use: _max_use}} = changeset) do
put_change(changeset, :invite_type, "reusable")
end

defp assign_type(changeset), do: put_change(changeset, :invite_type, "one_time")

Repo.insert(token)
@spec list_invites() :: [UserInviteToken.t()]
def list_invites do
query = from(u in UserInviteToken, order_by: u.id)
Repo.all(query)
end

def used_changeset(struct) do
struct
|> cast(%{}, [])
|> put_change(:used, true)
@spec update_invite!(UserInviteToken.t(), map()) :: UserInviteToken.t() | no_return()
def update_invite!(invite, changes) do
change(invite, changes) |> Repo.update!()
end

def mark_as_used(token) do
with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}
else
_e -> {:error, token}
@spec update_invite(UserInviteToken.t(), map()) ::
{:ok, UserInviteToken.t()} | {:error, Changeset.t()}
def update_invite(invite, changes) do
change(invite, changes) |> Repo.update()
end

@spec find_by_token!(token()) :: UserInviteToken.t() | no_return()
def find_by_token!(token), do: Repo.get_by!(UserInviteToken, token: token)

@spec find_by_token(token()) :: {:ok, UserInviteToken.t()} | nil
def find_by_token(token) do
with invite <- Repo.get_by(UserInviteToken, token: token) do
{:ok, invite}
end
end

@spec valid_invite?(UserInviteToken.t()) :: boolean()
def valid_invite?(%{invite_type: "one_time"} = invite) do
not invite.used
end

def valid_invite?(%{invite_type: "date_limited"} = invite) do
not_overdue_date?(invite) and not invite.used
end

def valid_invite?(%{invite_type: "reusable"} = invite) do
invite.uses < invite.max_use and not invite.used
end

def valid_invite?(%{invite_type: "reusable_date_limited"} = invite) do
not_overdue_date?(invite) and invite.uses < invite.max_use and not invite.used
end

defp not_overdue_date?(%{expires_at: expires_at}) do
Date.compare(Date.utc_today(), expires_at) in [:lt, :eq]
end

@spec update_usage!(UserInviteToken.t()) :: nil | UserInviteToken.t() | no_return()
def update_usage!(%{invite_type: "date_limited"}), do: nil

def update_usage!(%{invite_type: "one_time"} = invite),
do: update_invite!(invite, %{used: true})

def update_usage!(%{invite_type: invite_type} = invite)
when invite_type == "reusable" or invite_type == "reusable_date_limited" do
changes = %{
uses: invite.uses + 1
}

changes =
if changes.uses >= invite.max_use do
Map.put(changes, :used, true)
else
changes
end

update_invite!(invite, changes)
end
end

+ 79
- 112
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -7,13 +7,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger

import Ecto.Query
@@ -90,12 +91,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

def increase_replies_count_if_reply(%{
"object" =>
%{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object,
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Activity.increase_replies_count(reply_status_id)
Object.increase_replies_count(reply_ap_id)
end
end
@@ -103,25 +102,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_replies_count_if_reply(_create_data), do: :noop

def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Activity.decrease_replies_count(reply_status_id)
Object.decrease_replies_count(reply_ap_id)
end
end

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),
{:ok, object} <- insert_full_object(map) do
{recipients, _, _} = get_recipients(map)
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
@@ -146,8 +144,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

@@ -166,12 +179,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

if activity.data["type"] in ["Create"] do
activity.data["object"]
object = Object.normalize(activity)

object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)

if activity.data["object"]["attachment"] != [] do
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)

if activity.local do
@@ -183,14 +198,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_by_ap_id(activity.data["actor"]).follower_address
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
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

@@ -430,8 +449,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
:ok <- maybe_federate(activity) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Mailer.deliver_async()
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)

{:ok, activity}
@@ -474,7 +493,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

q
|> restrict_unlisted()
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end

@@ -553,37 +572,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_since(query, _), do: query

defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end

defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
activity in query,
where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end

defp restrict_tag_reject(query, _), do: query

defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end

defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
activity in query,
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end

defp restrict_tag_all(query, _), do: query

defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end

defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
activity in query,
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end

defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
activity in query,
where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end

@@ -617,26 +648,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end

defp restrict_limit(query, %{"limit" => limit}) do
from(activity in query, limit: ^limit)
end

defp restrict_limit(query, _), do: query

defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end

defp restrict_local(query, _), do: query

defp restrict_max(query, %{"max_id" => ""}), do: query

defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id)
end

defp restrict_max(query, _), do: query

defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
@@ -662,10 +679,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_favorited_by(query, _), do: query

defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end

defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end

@@ -707,7 +728,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
)
end
@@ -757,12 +785,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
from(
activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
)
base_query = from(activity in Activity)

base_query
|> maybe_preload_objects(opts)
@@ -772,8 +795,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_limit(opts)
|> restrict_max(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
@@ -789,14 +810,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end

def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc)
|> Repo.all()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end

@@ -861,7 +882,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
@@ -869,7 +890,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

def make_user_from_ap_id(ap_id) do
if _user = User.get_by_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
@@ -971,60 +992,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity)}
else
{:error, {:reject, nil}} ->
{:reject, nil}

object = %Object{} ->
{:ok, object}

_e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")

case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity)}
e -> e
end
end
end
end

def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching object #{id} via AP")

with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body),
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end

# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)


+ 6
- 4
lib/pleroma/web/activity_pub/activity_pub_controller.ex View File

@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectView
@@ -153,9 +154,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end

def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- Utils.recipient_in_message(user.ap_id, params),
params <- Utils.maybe_splice_recipient(user.ap_id, params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
@@ -172,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
"Signature missing or not from author, relayed Create message, fetching object from source"
)

ActivityPub.fetch_object_from_id(params["object"]["id"])
Fetcher.fetch_object_from_id(params["object"]["id"])

json(conn, "ok")
end


+ 2
- 2
lib/pleroma/web/activity_pub/relay.ex View File

@@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do

def follow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do

def unfollow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}


+ 103
- 113
lib/pleroma/web/activity_pub/transmogrifier.ex View File

@@ -8,8 +8,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"""
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -18,56 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

require Logger

def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end

def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end

def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end

def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end

@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error

def contain_origin(id, %{"actor" => _actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))

if id_uri.host == actor_uri.host do
:ok
else
:error
end
end

def contain_origin_from_id(_id, %{"id" => nil}), do: :error

def contain_origin_from_id(id, %{"id" => other_id} = _params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)

if id_uri.host == other_uri.host do
:ok
else
:error
end
end

@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
@@ -83,6 +35,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_content_map
|> fix_likes
|> fix_addressing
|> fix_summary
end

def fix_summary(%{"summary" => nil} = object) do
object
|> Map.put("summary", "")
end

def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end

def fix_summary(object) do
object
|> Map.put("summary", "")
end

def fix_addressing_list(map, field) do
@@ -158,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_implicit_addressing(object, _), do: object

def fix_addressing(object) do
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
{:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)

object
@@ -172,7 +140,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def fix_actor(%{"attributedTo" => actor} = object) do
object
|> Map.put("actor", get_actor(%{"actor" => actor}))
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
end

# Check for standardisation
@@ -207,14 +175,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
""
end

case fetch_obj_helper(in_reply_to_id) do
case get_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
with %Activity{} = activity <-
with %Activity{} = _activity <-
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
@@ -433,14 +400,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video", "Page"] do
actor = get_actor(data)
actor = Containment.get_actor(data)

data =
Map.put(data, "actor", actor)
|> fix_addressing

with nil <- Activity.get_create_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])

params = %{
@@ -469,30 +436,56 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
if not User.locked?(followed) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
{:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{:user_locked, false} <- {:user_locked, User.locked?(followed)},
{:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})

User.follow(follower, followed)
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")

ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})

{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")

ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})

{:user_locked, true} ->
:noop
end

{:ok, activity}
else
_e -> :error
_e ->
:error
end
end

def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@@ -517,8 +510,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@@ -541,9 +534,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
@@ -554,9 +547,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
@@ -570,7 +563,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
)
when object_type in ["Person", "Application", "Service", "Organization"] do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

banner = new_user_data[:info]["banner"]
@@ -609,10 +602,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
object_id = Utils.get_ap_id(object_id)

with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
:ok <- contain_origin(actor.ap_id, object.data),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
@@ -628,9 +621,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
@@ -647,7 +640,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = _data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
@@ -666,7 +659,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
@@ -680,7 +673,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
@@ -698,9 +691,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
@@ -710,9 +703,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def handle_incoming(_), do: :error

def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])

def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
@@ -749,9 +739,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon
# """

def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
object =
object
Object.normalize(object_id).data
|> prepare_object

data =
@@ -812,7 +802,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case fetch_obj_helper(data["object"]) do
case get_obj_helper(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
@@ -866,10 +856,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("tag", tags ++ mentions)
end

def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)

object
|> Map.put(:info, user_info)
end

# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(object) do
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
emoji = object["emoji"] || []

out =
emoji
@@ -887,6 +883,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("tag", tags ++ out)
end

def add_emoji_tags(object) do
object
end

def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
@@ -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
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
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{: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


+ 57
- 22
lib/pleroma/web/activity_pub/utils.ex View File

@@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
defp recipient_in_collection(_, _), do: false

def recipient_in_message(ap_id, params) do
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
cond do
recipient_in_collection(ap_id, params["to"]) ->
true
@@ -71,6 +71,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
!params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
true

# if the message is sent from somebody the user is following, then assume it
# is addressed to the recipient
User.following?(recipient, actor) ->
true

true ->
false
end
@@ -99,7 +104,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 +183,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 +212,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)
@@ -207,14 +234,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when is_map(object_data) and type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
{:ok, object}
map =
map
|> Map.put("object", object.data["id"])

{:ok, map, object}
end
end

def insert_full_object(_), do: {:ok, nil}
def insert_full_object(map), do: {:ok, map, nil}

def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
@@ -354,7 +385,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 +435,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,
activity.data,
^%{object: followed_id}
^followed_id
),
order_by: [desc: :id],
order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)

@@ -567,13 +600,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
)



+ 6
- 1
lib/pleroma/web/activity_pub/views/user_view.ex View File

@@ -69,6 +69,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do

endpoints = render("endpoints.json", %{user: user})

user_tags =
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])

%{
"id" => user.ap_id,
"type" => "Person",
@@ -87,7 +92,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"tag" => user.info.source_data["tag"] || []
"tag" => (user.info.source_data["tag"] || []) ++ user_tags
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))


+ 14
- 9
lib/pleroma/web/activity_pub/visibility.ex View File

@@ -41,16 +41,21 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
# guard
def entire_thread_visible_for_user?(nil, _user), do: false

# child
# XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength

def entire_thread_visible_for_user?(
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
%Activity{} = tail,
# %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
)
when is_binary(parent_id) do
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
) do
case Object.normalize(tail) do
%{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)

_ ->
visible_for_user?(tail, user)
end
end

# root
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
end

+ 58
- 14
lib/pleroma/web/admin_api/admin_api_controller.ex View File

@@ -5,6 +5,7 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Search
@@ -18,13 +19,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors)

def user_delete(conn, %{"nickname" => nickname}) do
User.get_by_nickname(nickname)
User.get_cached_by_nickname(nickname)
|> User.delete()

conn
|> json(nickname)
end

def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_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_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
end

conn
|> json("ok")
end

def user_create(
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
@@ -46,7 +67,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end

def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> json(AccountView.render("show.json", %{user: user}))
else
@@ -55,7 +76,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end

def user_toggle_activation(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)

{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)

@@ -110,7 +131,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)

info =
%{}
@@ -135,7 +156,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end

def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)

conn
|> json(%{
@@ -157,7 +178,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> put_status(403)
|> json(%{error: "You can't revoke your own admin status."})
else
user = User.get_by_nickname(nickname)
user = User.get_cached_by_nickname(nickname)

info =
%{}
@@ -183,7 +204,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_by_nickname(nickname),
%User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status),
do: json_response(conn, :no_content, "")
end
@@ -215,25 +236,48 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
with true <-
Pleroma.Config.get([:instance, :invites_enabled]) &&
!Pleroma.Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- Pleroma.UserInviteToken.create_token(),
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]),
{:ok, _} <- Pleroma.Mailer.deliver(email) do
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
end
end

@doc "Get a account registeration invite token (base64 string)"
def get_invite_token(conn, _params) do
{:ok, token} = Pleroma.UserInviteToken.create_token()
def get_invite_token(conn, params) do
options = params["invite"] || %{}
{:ok, invite} = UserInviteToken.create_invite(options)

conn
|> json(token.token)
|> json(invite.token)
end

@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()

conn
|> json(AccountView.render("invites.json", %{invites: invites}))
end

@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
invite = UserInviteToken.find_by_token!(token)
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true})

conn
|> json(AccountView.render("invite.json", %{invite: updated_invite}))
end

@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_by_nickname(nickname)
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)

conn


+ 18
- 0
lib/pleroma/web/admin_api/views/account_view.ex View File

@@ -26,4 +26,22 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"tags" => user.tags || []
}
end

def render("invite.json", %{invite: invite}) do
%{
"id" => invite.id,
"token" => invite.token,
"used" => invite.used,
"expires_at" => invite.expires_at,
"uses" => invite.uses,
"max_use" => invite.max_use,
"invite_type" => invite.invite_type
}
end

def render("invites.json", %{invites: invites}) do
%{
invites: render_many(invites, AccountView, "invite.json", as: :invite)
}
end
end

+ 48
- 2
lib/pleroma/web/auth/authenticator.ex View File

@@ -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
@@ -15,11 +16,56 @@ defmodule Pleroma.Web.Auth.Authenticator do
@callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()}
def get_user(plug), do: implementation().get_user(plug)

@callback create_from_registration(Plug.Conn.t(), Registration.t()) ::
{:ok, User.t()} | {:error, any()}
def create_from_registration(plug, registration),
do: implementation().create_from_registration(plug, registration)

@callback get_registration(Plug.Conn.t()) ::
{:ok, Registration.t()} | {:error, any()}
def get_registration(plug), do: implementation().get_registration(plug)

@callback handle_error(Plug.Conn.t(), any()) :: any()
def handle_error(plug, error), do: implementation().handle_error(plug, error)
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

@doc "Gets user by nickname or email for auth."
@spec fetch_user(String.t()) :: User.t() | nil
def fetch_user(name) do
User.get_by_nickname_or_email(name)
end

# Gets name and password from conn
#
@spec fetch_credentials(Plug.Conn.t() | map()) ::
{:ok, {name :: any, password :: any}} | {:error, :invalid_credentials}
def fetch_credentials(%Plug.Conn{params: params} = _),
do: fetch_credentials(params)

def fetch_credentials(params) do
case params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{:ok, {name, password}}

%{"grant_type" => "password", "username" => name, "password" => password} ->
{:ok, {name, password}}

_ ->
{:error, :invalid_credentials}
end
end
end

+ 23
- 29
lib/pleroma/web/auth/ldap_authenticator.ex View File

@@ -7,45 +7,39 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do

require Logger

import Pleroma.Web.Auth.Authenticator,
only: [fetch_credentials: 1, fetch_user: 1]

@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
if Pleroma.Config.get([:ldap, :enabled]) do
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}

%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end
defdelegate get_registration(conn), to: @base
defdelegate create_from_registration(conn, registration), to: @base
defdelegate handle_error(conn, error), to: @base
defdelegate auth_template, to: @base
defdelegate oauth_consumer_template, to: @base

case ldap_user(name, password) do
%User{} = user ->
{:ok, user}
def get_user(%Plug.Conn{} = conn) do
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
{:ok, {name, password}} <- fetch_credentials(conn),
%User{} = user <- ldap_user(name, password) do
{:ok, user}
else
{:error, {:ldap_connection_error, _}} ->
# When LDAP is unavailable, try default authenticator
@base.get_user(conn)

{:error, {:ldap_connection_error, _}} ->
# When LDAP is unavailable, try default authenticator
Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn)
{:ldap, _} ->
@base.get_user(conn)

error ->
error
end
else
# Fall back to default authenticator
Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn)
error ->
error
end
end

def handle_error(%Plug.Conn{} = _conn, error) do
error
end

def auth_template, do: nil

defp ldap_user(name, password) do
ldap = Pleroma.Config.get(:ldap, [])
host = Keyword.get(ldap, :host, "localhost")
@@ -93,7 +87,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do

case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
:ok ->
case User.get_by_nickname_or_email(name) do
case fetch_user(name) do
%User{} = user ->
user



+ 70
- 10
lib/pleroma/web/auth/pleroma_authenticator.ex View File

@@ -4,21 +4,18 @@

defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User

import Pleroma.Web.Auth.Authenticator,
only: [fetch_credentials: 1, fetch_user: 1]

@behaviour Pleroma.Web.Auth.Authenticator

def get_user(%Plug.Conn{} = conn) do
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}

%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end

with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
{:ok, user}
else
@@ -27,9 +24,72 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
end
end

def get_registration(%Plug.Conn{
assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
}) do
registration = Registration.get_by_provider_uid(provider, uid)

if registration do
{:ok, registration}
else
info = auth.info

%Registration{}
|> Registration.changeset(%{
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), do: {:error, :missing_credentials}

def create_from_registration(
%Plug.Conn{params: %{"authorization" => registration_attrs}},
registration
) do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])
name = value([registration_attrs["name"], Registration.name(registration)]) || nickname
bio = value([registration_attrs["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

+ 1
- 1
lib/pleroma/web/channels/user_socket.ex View File

@@ -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_cached_by_id(user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
_e -> :error


+ 32
- 17
lib/pleroma/web/common_api/common_api.ex View File

@@ -4,6 +4,7 @@

defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
@@ -125,7 +126,10 @@ defmodule Pleroma.Web.CommonAPI do
"public"

in_reply_to ->
Pleroma.Web.MastodonAPI.StatusView.get_visibility(in_reply_to.data["object"])
# XXX: these heuristics should be moved out of MastodonAPI.
with %Object{} = object <- Object.normalize(in_reply_to) do
Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
end
end
end

@@ -147,8 +151,8 @@ defmodule Pleroma.Web.CommonAPI do
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
cw <- data["spoiler_text"] || "",
full_payload <- String.trim(status <> cw),
length when length in 1..limit <- String.length(full_payload),
object <-
make_note_data(
@@ -166,19 +170,19 @@ defmodule Pleroma.Web.CommonAPI do
Map.put(
object,
"emoji",
(Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
|> Enum.reduce(%{}, fn {name, file}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
Formatter.get_emoji_map(full_payload)
) 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
@@ -211,8 +215,10 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{
actor: ^user_ap_id,
data: %{
"type" => "Create",
"object" => %{
"type" => "Create"
},
object: %Object{
data: %{
"to" => object_to,
"type" => "Note"
}
@@ -274,9 +280,18 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def bookmarked?(user, activity) do
with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
true
else
_ ->
false
end
end

def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-


+ 51
- 12
lib/pleroma/web/common_api/utils.ex View File

@@ -12,9 +12,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
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 +34,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
@@ -180,6 +183,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end

@doc """
Formatting text as BBCode.
"""
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end

@doc """
Formatting text to html.
"""
def format_input(text, "text/html", options) do
@@ -192,11 +207,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Formatting text to markdown.
"""
def format_input(text, "text/markdown", options) do
options = Keyword.put(options, :mentions_escape, true)

text
|> Formatter.mentions_escape(options)
|> Earmark.as_html!()
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} -> {Earmark.as_html!(text), mentions, tags} end).()
|> Formatter.html_escape("text/html")
end

@@ -206,7 +220,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
context,
content_html,
attachments,
inReplyTo,
in_reply_to,
tags,
cw \\ nil,
cc \\ []
@@ -223,10 +237,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}

if inReplyTo do
if in_reply_to do
in_reply_to_object = Object.normalize(in_reply_to)

object
|> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
|> Map.put("inReplyToStatusId", inReplyTo.id)
|> Map.put("inReplyTo", in_reply_to_object.data["id"])
else
object
end
@@ -240,15 +255,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 +296,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_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
@@ -285,7 +306,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}"},
@@ -327,6 +348,24 @@ defmodule Pleroma.Web.CommonAPI.Utils do

def maybe_notify_mentioned_recipients(recipients, _), do: recipients

def maybe_notify_subscribers(
recipients,
%Activity{data: %{"actor" => actor, "type" => type}} = activity
)
when type == "Create" do
with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscribers()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)

recipients ++ subscriber_ids
end
end

def maybe_notify_subscribers(recipients, _), do: recipients

def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)


+ 5
- 0
lib/pleroma/web/controller_helper.ex View File

@@ -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


+ 29
- 4
lib/pleroma/web/endpoint.ex View File

@@ -51,11 +51,17 @@ 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"

extra =
Pleroma.Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")

# 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 +71,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: extra
)

# 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 """


+ 3
- 2
lib/pleroma/web/federator/federator.ex View File

@@ -4,6 +4,7 @@

defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
@@ -136,7 +137,7 @@ defmodule Pleroma.Web.Federator do
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
:ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
:ok <- Containment.contain_origin_from_id(params["actor"], params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
@@ -185,7 +186,7 @@ defmodule Pleroma.Web.Federator do
end

def ap_enabled_actor(id) do
user = User.get_by_ap_id(id)
user = User.get_cached_by_ap_id(id)

if User.ap_enabled?(user) do
{:ok, user}


+ 34
- 1
lib/pleroma/web/mastodon_api/mastodon_api.ex View File

@@ -5,7 +5,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI

def follow(follower, followed, params \\ %{}) do
options = cast_params(params)
reblogs = options[:reblogs]

result =
if not User.following?(follower, followed) do
CommonAPI.follow(follower, followed)
else
{:ok, follower, followed, nil}
end

with {:ok, follower, followed, _} <- result do
reblogs
|> case do
false -> CommonAPI.hide_reblogs(follower, followed)
_ -> CommonAPI.show_reblogs(follower, followed)
end
|> case do
{:ok, follower} -> {:ok, follower}
_ -> {:ok, follower}
end
end
end

def get_followers(user, params \\ %{}) do
user
@@ -28,9 +54,16 @@ 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}
exclude_types: {:array, :string},
reblogs: :boolean
}

changeset = cast({%{}, param_types}, params, Map.keys(param_types))


+ 323
- 94
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex View File

@@ -4,13 +4,18 @@

defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@@ -25,13 +30,14 @@ 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
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token

import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
alias Pleroma.Web.ControllerHelper
import Ecto.Query

require Logger
@@ -42,7 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
action_fallback(:errors)

def create_app(conn, params) do
scopes = oauth_scopes(params, ["read"])
scopes = ControllerHelper.oauth_scopes(params, ["read"])

app_attrs =
params
@@ -81,7 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user_params =
%{}
|> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
|> add_if_present(params, "avatar", :avatar, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
@@ -91,9 +97,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end)

emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")

user_info_emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()

info_params =
%{}
|> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
[:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)}
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
@@ -102,8 +119,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> :error
end
end)
|> Map.put(:emoji, user_info_emojis)

info_cng = User.Info.mastodon_profile_update(user.info, info_params)
info_cng = User.Info.profile_update(user.info, info_params)

with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
@@ -178,14 +196,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" => tags
}
end)
end
@@ -198,15 +217,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id"])
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)

last = List.last(activities)
first = List.first(activities)

if last do
min = last.id
max = first.id
max_id = last.id

limit =
params
|> Map.get("limit", "20")
|> String.to_integer()

min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end

{next_url, prev_url} =
if param do
@@ -215,13 +248,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: min})
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{since_id: max})
Map.merge(params, %{min_id: min_id})
)
}
else
@@ -229,12 +262,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: min})
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{since_id: max})
Map.merge(params, %{min_id: min_id})
)
}
end
@@ -260,6 +293,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()

user = Repo.preload(user, bookmarks: :activity)

conn
|> add_link_headers(:home_timeline, activities)
|> put_view(StatusView)
@@ -278,6 +313,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()

user = Repo.preload(user, bookmarks: :activity)

conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView)
@@ -285,7 +322,8 @@ 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_cached_by_id(params["id"]),
reading_user <- Repo.preload(reading_user, :bookmarks) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params)

conn
@@ -310,7 +348,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Repo.all()
|> Pagination.fetch_paginated(params)

user = Repo.preload(user, bookmarks: :activity)

conn
|> add_link_headers(:dm_timeline, activities)
@@ -319,8 +359,10 @@ 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_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
@@ -328,7 +370,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 +406,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 +475,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
@@ -404,7 +510,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end

def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
@@ -413,7 +522,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do

def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -460,10 +571,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end

def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -471,10 +584,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end

def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -544,6 +659,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end

def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
@@ -592,27 +712,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^likes)
users = Repo.all(q)

conn
|> put_view(AccountView)
|> render(AccountView, "accounts.json", %{users: users, as: :user})
|> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
end

def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)

conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
|> render("accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
@@ -657,7 +779,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_cached_by_id(id),
followers <- MastodonAPI.get_followers(user, params) do
followers =
cond do
@@ -669,12 +791,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> add_link_headers(:followers, followers, user)
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end

def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- Repo.get(User, id),
with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_friends(user, params) do
followers =
cond do
@@ -686,7 +808,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> add_link_headers(:following, followers, user)
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
|> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end

@@ -694,12 +816,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: follow_requests, as: :user})
|> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end
end

def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@@ -713,7 +835,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_cached_by_id(id),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@@ -727,25 +849,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end

def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
false <- User.following?(follower, followed),
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
else
true ->
followed = User.get_cached_by_id(id)

{:ok, follower} =
case conn.params["reblogs"] do
true -> CommonAPI.show_reblogs(follower, followed)
false -> CommonAPI.hide_reblogs(follower, followed)
end

conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
{:followed, _} ->
{:error, :not_found}

{:error, message} ->
conn
@@ -755,12 +867,16 @@ 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} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("account.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}

{:error, message} ->
conn
|> put_resp_content_type("application/json")
@@ -769,16 +885,23 @@ 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} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower} <- CommonAPI.unfollow(follower, followed) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
else
{:followed, _} ->
{:error, :not_found}

error ->
error
end
end

def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.mute(muter, muted) do
conn
|> put_view(AccountView)
@@ -792,7 +915,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_cached_by_id(id),
{:ok, muter} <- User.unmute(muter, muted) do
conn
|> put_view(AccountView)
@@ -813,7 +936,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_cached_by_id(id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
conn
@@ -828,7 +951,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_cached_by_id(id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
conn
@@ -863,10 +986,38 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end

def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %User{} = subscription_target <- User.get_cached_by_id(id),
{:ok, subscription_target} = User.subscribe(user, subscription_target) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: subscription_target})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end

def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %User{} = subscription_target <- User.get_cached_by_id(id),
{:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: subscription_target})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end

def status_search(user, query) do
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
with {:ok, object} <- Fetcher.fetch_object_from_id(query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
[activity]
@@ -877,13 +1028,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do

q =
from(
a in Activity,
[a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^query
),
limit: 20,
@@ -959,21 +1110,65 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
ActivityPub.fetch_activities([], params)
|> Enum.reverse()

user = Repo.preload(user, bookmarks: :activity)

conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end

def bookmarks(%{assigns: %{user: user}} = conn, _) do
user = Repo.get(User, user.id)
def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- User.get_by_id(id),
false <- user.info.hide_favorites do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", for_user)

recipients =
if for_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[for_user.ap_id | for_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end

activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()

conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: for_user, as: :activity})
else
nil ->
{:error, :not_found}

true ->
conn
|> put_status(403)
|> json(%{error: "Can't get favorites"})
end
end

def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
user = Repo.preload(user, bookmarks: :activity)

bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)

activities =
user.bookmarks
|> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
|> Enum.reverse()
bookmarks
|> Enum.map(fn b -> b.activity end)

conn
|> add_link_headers(:bookmarks, bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
@@ -1023,7 +1218,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_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
end
end)
@@ -1035,7 +1230,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_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
@@ -1048,7 +1243,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, users} = Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
|> render("accounts.json", %{for: user, users: users, as: :user})
end
end

@@ -1079,6 +1274,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()

user = Repo.preload(user, bookmarks: :activity)

conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
@@ -1091,9 +1288,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()
@@ -1108,8 +1303,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
initial_state =
%{
meta: %{
streaming_api_base_url:
String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
@@ -1121,7 +1315,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 +1388,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 +1445,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 +1473,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 +1524,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_cached_by_id(id) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: target})
@@ -1390,6 +1602,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)
@@ -1431,7 +1660,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
x,
"id",
case User.get_or_fetch(x["acct"]) do
%{id: id} -> id
{:ok, %User{id: id}} -> id
_ -> 0
end
)
@@ -1454,7 +1683,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(


+ 44
- 5
lib/pleroma/web/mastodon_api/views/account_view.ex View File

@@ -53,6 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
blocking: User.blocks?(user, target),
muting: User.mutes?(user, target),
muting_notifications: false,
subscribing: User.subscribed_to?(user, target),
requested: requested,
domain_blocking: false,
showing_reblogs: User.showing_reblogs?(user, target),
@@ -67,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp do_render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
user_info = User.get_cached_user_info(user)
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]

emojis =
@@ -112,19 +113,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot: bot,
source: %{
note: "",
privacy: user_info.default_scope,
sensitive: false
sensitive: false,
pleroma: %{}
},

# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin,
hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites,
relationship: relationship
}
}
|> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info)
|> maybe_put_notification_settings(user, opts[:for])
end

defp username_from_nickname(string) when is_binary(string) do
@@ -132,4 +137,38 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end

defp username_from_nickname(_), do: nil

defp maybe_put_settings(
data,
%User{id: user_id} = user,
%User{id: user_id},
user_info
) do
data
|> Kernel.put_in([:source, :privacy], user_info.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text)
end

defp maybe_put_settings(data, _, _, _), do: data

defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end

defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end

defp maybe_put_role(data, _, _), do: data

defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)
end

defp maybe_put_notification_settings(data, _, _), do: data
end

+ 57
- 0
lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex View File

@@ -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

+ 97
- 45
lib/pleroma/web/mastodon_api/views/status_view.ex View File

@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do

alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -19,8 +20,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} ->
in_reply_to != "" && in_reply_to
%{data: %{"type" => "Create", "object" => object}} ->
object = Object.normalize(object)
object.data["inReplyTo"] != "" && object.data["inReplyTo"]

_ ->
nil
@@ -29,7 +31,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.create_by_object_ap_id()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
Map.put(acc, activity.data["object"]["id"], activity)
object = Object.normalize(activity)
Map.put(acc, object.data["id"], activity)
end)
end

@@ -54,6 +57,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do

defp get_context_id(_), do: nil

defp reblogged?(activity, user) do
object = Object.normalize(activity) || %{}
present?(user && user.ap_id in (object.data["announcements"] || []))
end

def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)

@@ -72,8 +80,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])

reblogged = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged))
reblogged_activity = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))

activity_object = Object.normalize(activity)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])

bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], reblogged_activity)

mentions =
activity.recipients
@@ -94,9 +107,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: false,
favourited: false,
bookmarked: false,
reblogged: reblogged?(reblogged_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
sensitive: false,
@@ -117,14 +130,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do
def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)

user = get_user(activity.data["actor"])

like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0

tags = object["tag"] || []
sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
tags = object.data["tag"] || []
sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")

mentions =
activity.recipients
@@ -132,25 +147,53 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity)

attachment_data = object["attachment"] || []
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)

created_at = Utils.to_masto_date(object["published"])
created_at = Utils.to_masto_date(object.data["published"])

reply_to = get_reply_to(activity, opts)

reply_to_user = reply_to && get_user(reply_to.data["actor"])

content =
object
|> render_content()
|> HTML.get_cached_scrubbed_html_for_object(

content_html =
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content"
)

content_plaintext =
content
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content"
)

summary = object.data["summary"] || ""

summary_html =
summary
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
__MODULE__
"mastoapi:summary"
)

summary_plaintext =
summary
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:summary"
)

card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
@@ -159,30 +202,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object["external_url"] || object["id"]
object.data["external_url"] || object.data["id"]
end

%{
id: to_string(activity.id),
uri: object["id"],
uri: object.data["id"],
url: url,
account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content,
content: content_html,
created_at: created_at,
reblogs_count: announcement_count,
replies_count: object["repliesCount"] || 0,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: present?(repeated),
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: object["summary"] || "",
spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
mentions: mentions,
@@ -192,10 +235,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
website: nil
},
language: nil,
emojis: build_emojis(activity.data["object"]["emoji"]),
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity)
conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext}
}
}
end
@@ -272,13 +318,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end

def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
_id = activity.data["object"]["inReplyTo"]
replied_to_activities[activity.data["object"]["inReplyTo"]]
object = Object.normalize(activity)

with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end

def get_reply_to(%{data: %{"object" => object}}, _) do
if object["inReplyTo"] && object["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object["inReplyTo"])
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity)

if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
@@ -286,8 +338,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do

def get_visibility(object) do
public = "https://www.w3.org/ns/activitystreams#Public"
to = object["to"] || []
cc = object["cc"] || []
to = object.data["to"] || []
cc = object.data["cc"] || []

cond do
public in to ->
@@ -308,25 +360,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end

def render_content(%{"type" => "Video"} = object) do
with name when not is_nil(name) and name != "" <- object["name"] do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
def render_content(%{data: %{"type" => "Video"}} = object) do
with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
_ -> object["content"] || ""
_ -> object.data["content"] || ""
end
end

def render_content(%{"type" => object_type} = object)
def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Article", "Page"] do
with summary when not is_nil(summary) and summary != "" <- object["name"],
url when is_bitstring(url) <- object["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}"
with summary when not is_nil(summary) and summary != "" <- object.data["name"],
url when is_bitstring(url) <- object.data["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
else
_ -> object["content"] || ""
_ -> object.data["content"] || ""
end
end

def render_content(object), do: object["content"] || ""
def render_content(object), do: object.data["content"] || ""

@doc """
Builds a dictionary tags.


+ 1
- 1
lib/pleroma/web/mastodon_api/websocket_handler.ex View File

@@ -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_cached_by_id(user_id) do
{:ok, user}
else
_ -> {:error, 403}


+ 31
- 19
lib/pleroma/web/media_proxy/media_proxy.ex View File

@@ -13,32 +13,44 @@ defmodule Pleroma.Web.MediaProxy do

def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
domain = URI.parse(url).host

if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do
url
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]

# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")

# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
cond do
!Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)

sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
String.equivalent?(domain, pattern)
end) ->
url

build_url(sig64, base64, filename(url))
true ->
encode_url(url)
end
end

def encode_url(url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]

# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")

# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)

sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)

build_url(sig64, base64, filename(url))
end

def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)


+ 13
- 0
lib/pleroma/web/metadata/rel_me.ex View File

@@ -0,0 +1,13 @@
defmodule Pleroma.Web.Metadata.Providers.RelMe do
alias Pleroma.Web.Metadata.Providers.Provider
@behaviour Provider

@impl Provider
def build_tags(%{user: user}) do
(Floki.attribute(user.bio, "link[rel~=me]", "href") ++
Floki.attribute(user.bio, "a[rel~=me]", "href"))
|> Enum.map(fn link ->
{:link, [rel: "me", href: link], []}
end)
end
end

+ 1
- 1
lib/pleroma/web/metadata/utils.ex View File

@@ -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


+ 16
- 3
lib/pleroma/web/oauth/fallback_controller.ex View File

@@ -6,11 +6,24 @@ 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")
|> OAuthController.authorize(conn.params["authorization"])
|> OAuthController.authorize(conn.params)
end
end

+ 280
- 60
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -5,25 +5,57 @@
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
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end

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

# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
client_id: params["client_id"],
@@ -35,72 +67,75 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
end

def create_authorization(conn, %{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_params
def create_authorization(
conn,
%{"authorization" => _} = params,
opts \\ []
) do
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
after_create_authorization(conn, auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
end
end

def after_create_authorization(conn, auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) 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
redirect_uri = redirect_uri(conn, redirect_uri)

cond do
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
render(conn, "results.html", %{
auth: auth
})
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_attrs["state"] do
Map.put(url_params, :state, auth_attrs["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, _},
%{"authorization" => _} = 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(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},
%{"authorization" => _} = 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(params)
end

error ->
Authenticator.handle_error(conn, error)
end
defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end

def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
@@ -108,7 +143,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_cached_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 = %{
@@ -136,6 +171,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
%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 +195,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 +230,175 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end

@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
scope =
oauth_scopes(auth_attrs, [])
|> Enum.join(" ")

state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
|> Poison.encode!()

params =
auth_attrs
|> 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) do
user = Repo.preload(registration, :user).user
auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))

if user do
create_authorization(
conn,
%{"authorization" => auth_attrs},
user: user
)
else
registration_params =
Map.merge(auth_attrs, %{
"nickname" => Registration.nickname(registration),
"email" => Registration.email(registration)
})

conn
|> put_session(:registration_id, registration.id)
|> registration_details(%{"authorization" => 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, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
scopes: oauth_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
end

def register(conn, %{"authorization" => _, "op" => "connect"} = params) do
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, 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, params)
else
{:create_authorization, error} ->
{:register, handle_create_authorization_error(conn, error, params)}

_ ->
{:register, :generic_error}
end
end

def register(conn, %{"authorization" => _, "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, registration) do
conn
|> put_session_registration_id(nil)
|> create_authorization(
params,
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_attrs
},
user \\ nil
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, 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_attrs, []),
{: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 +431,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

+ 1
- 1
lib/pleroma/web/oauth/token.ex View File

@@ -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_cached_by_id(auth.user_id), auth.scopes)
end
end



+ 18
- 24
lib/pleroma/web/ostatus/activity_representer.ex View File

@@ -54,23 +54,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
end

defp get_links(%{local: true, data: data}) do
defp get_links(%{local: true}, %{"id" => object_id}) do
h = fn str -> [to_charlist(str)] end

[
{:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
{:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
]
end

defp get_links(%{
local: false,
data: %{
"object" => %{
"external_url" => external_url
}
}
}) do
defp get_links(%{local: false}, %{"external_url" => external_url}) do
h = fn str -> [to_charlist(str)] end

[
@@ -78,7 +71,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
]
end

defp get_links(_activity), do: []
defp get_links(_activity, _object_data), do: []

defp get_emoji_links(emojis) do
Enum.map(emojis, fn {emoji, file} ->
@@ -88,14 +81,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do

def to_simple_form(activity, user, with_author \\ false)

def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end

updated_at = activity.data["object"]["published"]
inserted_at = activity.data["object"]["published"]
object = Object.normalize(activity)

updated_at = object.data["published"]
inserted_at = object.data["published"]

attachments =
Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
Enum.map(object.data["attachment"] || [], fn attachment ->
url = hd(attachment["url"])

{:link,
@@ -108,7 +103,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
mentions = activity.recipients |> get_mentions

categories =
(activity.data["object"]["tag"] || [])
(object.data["tag"] || [])
|> Enum.map(fn tag ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
@@ -118,11 +113,11 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
|> Enum.filter(& &1)

emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
emoji_links = get_emoji_links(object.data["emoji"] || %{})

summary =
if activity.data["object"]["summary"] do
[{:summary, [], h.(activity.data["object"]["summary"])}]
if object.data["summary"] do
[{:summary, [], h.(object.data["summary"])}]
else
[]
end
@@ -131,10 +126,9 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
# For notes, federate the object id.
{:id, h.(activity.data["object"]["id"])},
{:id, h.(object.data["id"])},
{:title, ['New note by #{user.nickname}']},
{:content, [type: 'html'],
h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))},
{:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
@@ -142,7 +136,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++
summary ++
get_links(activity) ++
get_links(activity, object.data) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end



+ 3
- 2
lib/pleroma/web/ostatus/handlers/note_handler.ex View File

@@ -113,8 +113,9 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
cw <- OStatus.get_cw(entry),
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
in_reply_to <-
(in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to,
in_reply_to_object <-
(in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
attachments <- OStatus.get_attachments(entry),
context <- get_context(entry, in_reply_to),
tags <- OStatus.get_tags(entry),


+ 1
- 1
lib/pleroma/web/ostatus/ostatus.ex View File

@@ -294,7 +294,7 @@ defmodule Pleroma.Web.OStatus do
}

with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do
%User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
{:ok, user}
else
_e -> User.insert_or_update_user(data)


+ 17
- 14
lib/pleroma/web/push/impl.ex View File

@@ -19,10 +19,12 @@ 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(
%{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =
notif
@spec perform(Notification.t()) :: list(any) | :error
def perform(
%{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
user_id: user_id
} = notif
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
@@ -30,13 +32,14 @@ defmodule Pleroma.Web.Push.Impl do
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)

for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
body: format_body(notif, actor),
body: format_body(notif, actor, object),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
@@ -50,7 +53,7 @@ defmodule Pleroma.Web.Push.Impl do
end
end

def perform_send(_) do
def perform(_) do
Logger.warn("Unknown notification type")
:error
end
@@ -95,25 +98,25 @@ defmodule Pleroma.Web.Push.Impl do
end

def format_body(
%{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}},
actor
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end

def format_body(
%{activity: %{data: %{"type" => "Announce", "object" => activity_id}}},
actor
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}}
) do
%Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
%Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)

"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end

def format_body(
%{activity: %{data: %{"type" => type}}},
actor
actor,
_object
)
when type in ["Follow", "Like"] do
case type do


+ 10
- 38
lib/pleroma/web/push/push.ex View File

@@ -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

+ 2
- 1
lib/pleroma/web/rel_me.ex View File

@@ -6,7 +6,8 @@ defmodule Pleroma.Web.RelMe do
@hackney_options [
pool: :media,
recv_timeout: 2_000,
max_body: 2_000_000
max_body: 2_000_000,
with_body: true
]

if Mix.env() == :test do


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save