Merge branch 'release/2.1.0' into 'stable'

Release/2.1.0

See merge request pleroma/pleroma!2927
This commit is contained in:
lain 2020-08-28 12:17:19 +00:00
commit 73dd5bdb7d
1601 changed files with 59471 additions and 22297 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
/*.ez /*.ez
/test/uploads /test/uploads
/.elixir_ls /.elixir_ls
/test/fixtures/DSCN0010_tmp.jpg
/test/fixtures/test_tmp.txt /test/fixtures/test_tmp.txt
/test/fixtures/image_tmp.jpg /test/fixtures/image_tmp.jpg
/test/tmp/ /test/tmp/

View File

@ -1,4 +1,4 @@
image: elixir:1.8.1 image: elixir:1.9.4
variables: &global_variables variables: &global_variables
POSTGRES_DB: pleroma_test POSTGRES_DB: pleroma_test
@ -22,6 +22,7 @@ stages:
- docker - docker
before_script: before_script:
- apt-get update && apt-get install -y cmake
- mix local.hex --force - mix local.hex --force
- mix local.rebar --force - mix local.rebar --force
@ -48,6 +49,7 @@ benchmark:
unit-testing: unit-testing:
stage: test stage: test
retry: 2
cache: &testing_cache_policy cache: &testing_cache_policy
<<: *global_cache_policy <<: *global_cache_policy
policy: pull policy: pull
@ -57,6 +59,7 @@ unit-testing:
alias: postgres alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script: script:
- apt-get update && apt-get install -y libimage-exiftool-perl
- mix deps.get - mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
@ -80,6 +83,7 @@ unit-testing:
unit-testing-rum: unit-testing-rum:
stage: test stage: test
retry: 2
cache: *testing_cache_policy cache: *testing_cache_policy
services: services:
- name: minibikini/postgres-with-rum:12 - name: minibikini/postgres-with-rum:12
@ -89,6 +93,7 @@ unit-testing-rum:
<<: *global_variables <<: *global_variables
RUM_ENABLED: "true" RUM_ENABLED: "true"
script: script:
- apt-get update && apt-get install -y libimage-exiftool-perl
- mix deps.get - mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
@ -168,8 +173,7 @@ stop_review_app:
amd64: amd64:
stage: release stage: release
# TODO: Replace with upstream image when 1.9.0 comes out image: elixir:1.10.3
image: rinpatch/elixir:1.9.0-rc.0
only: &release-only only: &release-only
- stable@pleroma/pleroma - stable@pleroma/pleroma
- develop@pleroma/pleroma - develop@pleroma/pleroma
@ -192,6 +196,7 @@ amd64:
variables: &release-variables variables: &release-variables
MIX_ENV: prod MIX_ENV: prod
before_script: &before-release before_script: &before-release
- apt-get update && apt-get install -y cmake
- echo "import Mix.Config" > config/prod.secret.exs - echo "import Mix.Config" > config/prod.secret.exs
- mix local.hex --force - mix local.hex --force
- mix local.rebar --force - mix local.rebar --force
@ -206,12 +211,11 @@ amd64-musl:
stage: release stage: release
artifacts: *release-artifacts artifacts: *release-artifacts
only: *release-only only: *release-only
# TODO: Replace with upstream image when 1.9.0 comes out image: elixir:1.10.3-alpine
image: rinpatch/elixir:1.9.0-rc.0-alpine
cache: *release-cache cache: *release-cache
variables: *release-variables variables: *release-variables
before_script: &before-release-musl before_script: &before-release-musl
- apk add git gcc g++ musl-dev make - apk add git gcc g++ musl-dev make cmake
- echo "import Mix.Config" > config/prod.secret.exs - echo "import Mix.Config" > config/prod.secret.exs
- mix local.hex --force - mix local.hex --force
- mix local.rebar --force - mix local.rebar --force
@ -223,8 +227,7 @@ arm:
only: *release-only only: *release-only
tags: tags:
- arm32 - arm32
# TODO: Replace with upstream image when 1.9.0 comes out image: elixir:1.10.3
image: rinpatch/elixir:1.9.0-rc.0-arm
cache: *release-cache cache: *release-cache
variables: *release-variables variables: *release-variables
before_script: *before-release before_script: *before-release
@ -236,8 +239,7 @@ arm-musl:
only: *release-only only: *release-only
tags: tags:
- arm32 - arm32
# TODO: Replace with upstream image when 1.9.0 comes out image: elixir:1.10.3-alpine
image: rinpatch/elixir:1.9.0-rc.0-arm-alpine
cache: *release-cache cache: *release-cache
variables: *release-variables variables: *release-variables
before_script: *before-release-musl before_script: *before-release-musl
@ -249,8 +251,7 @@ arm64:
only: *release-only only: *release-only
tags: tags:
- arm - arm
# TODO: Replace with upstream image when 1.9.0 comes out image: elixir:1.10.3
image: rinpatch/elixir:1.9.0-rc.0-arm64
cache: *release-cache cache: *release-cache
variables: *release-variables variables: *release-variables
before_script: *before-release before_script: *before-release
@ -263,7 +264,7 @@ arm64-musl:
tags: tags:
- arm - arm
# TODO: Replace with upstream image when 1.9.0 comes out # TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine image: elixir:1.10.3-alpine
cache: *release-cache cache: *release-cache
variables: *release-variables variables: *release-variables
before_script: *before-release-musl before_script: *before-release-musl
@ -281,6 +282,8 @@ docker:
IMAGE_TAG_SLUG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG IMAGE_TAG_SLUG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
IMAGE_TAG_LATEST: $CI_REGISTRY_IMAGE:latest IMAGE_TAG_LATEST: $CI_REGISTRY_IMAGE:latest
IMAGE_TAG_LATEST_STABLE: $CI_REGISTRY_IMAGE:latest-stable IMAGE_TAG_LATEST_STABLE: $CI_REGISTRY_IMAGE:latest-stable
DOCKER_BUILDX_URL: https://github.com/docker/buildx/releases/download/v0.4.1/buildx-v0.4.1.linux-amd64
DOCKER_BUILDX_HASH: 71a7d01439aa8c165a25b59c44d3f016fddbd98b
before_script: &before-docker before_script: &before-docker
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull $IMAGE_TAG_SLUG || true - docker pull $IMAGE_TAG_SLUG || true
@ -288,10 +291,14 @@ docker:
- export CI_VCS_REF=$CI_COMMIT_SHORT_SHA - export CI_VCS_REF=$CI_COMMIT_SHORT_SHA
allow_failure: true allow_failure: true
script: script:
- docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . - mkdir -p /root/.docker/cli-plugins
- docker push $IMAGE_TAG - wget "${DOCKER_BUILDX_URL}" -O ~/.docker/cli-plugins/docker-buildx
- docker push $IMAGE_TAG_SLUG - echo "${DOCKER_BUILDX_HASH} /root/.docker/cli-plugins/docker-buildx" | sha1sum -c
- docker push $IMAGE_TAG_LATEST - chmod +x ~/.docker/cli-plugins/docker-buildx
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker buildx create --name mbuilder --driver docker-container --use
- docker buildx inspect --bootstrap
- docker buildx build --platform linux/amd64,linux/arm/v7,linux/arm64/v8 --push --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST .
tags: tags:
- dind - dind
only: only:
@ -306,10 +313,14 @@ docker-stable:
before_script: *before-docker before_script: *before-docker
allow_failure: true allow_failure: true
script: script:
- docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . - mkdir -p /root/.docker/cli-plugins
- docker push $IMAGE_TAG - wget "${DOCKER_BUILDX_URL}" -O ~/.docker/cli-plugins/docker-buildx
- docker push $IMAGE_TAG_SLUG - echo "${DOCKER_BUILDX_HASH} /root/.docker/cli-plugins/docker-buildx" | sha1sum -c
- docker push $IMAGE_TAG_LATEST_STABLE - chmod +x ~/.docker/cli-plugins/docker-buildx
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker buildx create --name mbuilder --driver docker-container --use
- docker buildx inspect --bootstrap
- docker buildx build --platform linux/amd64,linux/arm/v7,linux/arm64/v8 --push --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE .
tags: tags:
- dind - dind
only: only:
@ -324,9 +335,15 @@ docker-release:
before_script: *before-docker before_script: *before-docker
allow_failure: true allow_failure: true
script: script:
- docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . script:
- docker push $IMAGE_TAG - mkdir -p /root/.docker/cli-plugins
- docker push $IMAGE_TAG_SLUG - wget "${DOCKER_BUILDX_URL}" -O ~/.docker/cli-plugins/docker-buildx
- echo "${DOCKER_BUILDX_HASH} /root/.docker/cli-plugins/docker-buildx" | sha1sum -c
- chmod +x ~/.docker/cli-plugins/docker-buildx
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker buildx create --name mbuilder --driver docker-container --use
- docker buildx inspect --bootstrap
- docker buildx build --platform linux/amd64,linux/arm/v7,linux/arm64/v8 --push --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG .
tags: tags:
- dind - dind
only: only:

View File

@ -0,0 +1,18 @@
<!--
### Precheck
* For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels).
* Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it.
* Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository.
-->
### Environment
* Installation type (OTP or From Source):
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system:
* PostgreSQL version (`psql -V`):
### Bug description

View File

@ -0,0 +1,6 @@
### Release checklist
* [ ] Bump version in `mix.exs`
* [ ] Compile a changelog
* [ ] Create an MR with an announcement to pleroma.social
* [ ] Tag the release
* [ ] Merge `stable` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)

View File

@ -3,6 +3,119 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.1.0] - 2020-08-28
### Changed
- **Breaking:** The default descriptions on uploads are now empty. The old behavior (filename as default) can be configured, see the cheat sheet.
- **Breaking:** Added the ObjectAgePolicy to the default set of MRFs. This will delist and strip the follower collection of any message received that is older than 7 days. This will stop users from seeing very old messages in the timelines. The messages can still be viewed on the user's page and in conversations. They also still trigger notifications.
- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
- **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated.
- **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated.
- **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated.
- **Breaking** Changed defaults for `:restrict_unauthenticated` so that when `:instance, :public` is set to `false` then all `:restrict_unauthenticated` items be effectively set to `true`. If you'd like to allow unauthenticated access to specific API endpoints on a private instance, please explicitly set `:restrict_unauthenticated` to non-default value in `config/prod.secret.exs`.
- In Conversations, return only direct messages as `last_status`
- Using the `only_media` filter on timelines will now exclude reblog media
- MFR policy to set global expiration for all local Create activities
- OGP rich media parser merged with TwitterCard
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
<details>
<summary>API Changes</summary>
- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed.
- **Breaking:** Image description length is limited now.
- **Breaking:** Emoji API: changed methods and renamed routes.
- **Breaking:** Notification Settings API for suppressing notifications has been simplified down to `block_from_strangers`.
- **Breaking:** Notification Settings API option for hiding push notification contents has been renamed to `hide_notification_contents`.
- MastodonAPI: Allow removal of avatar, banner and background.
- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream.
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
- Mastodon API: Added `pleroma.metadata.post_formats` to /api/v1/instance
- Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone`
- Mastodon API: Make notifications about statuses from muted users and threads read automatically
- Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value.
</details>
<details>
<summary>Admin API Changes</summary>
- **Breaking** Changed relay `/api/pleroma/admin/relay` endpoints response format.
- Status visibility stats: now can return stats per instance.
- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
</details>
### Removed
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- Frontends: Add mix task to install frontends.
- Frontends: Add configurable frontends for primary and admin fe.
- Configuration: Added a blacklist for email servers.
- Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation.
- Chats: Added support for federated chats. For details, see the docs.
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- Configuration: Add `:database_config_whitelist` setting to whitelist settings which can be configured from AdminFE.
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Mix task to reset MFA for user accounts
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
- Support pagination in emoji packs API (for packs and for files in pack)
- Support for viewing instances favicons next to posts and accounts
- Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
- "By approval" registrations mode.
- Configuration: Added `:welcome` settings for the welcome message to newly registered users. You can send a welcome message as a direct message, chat or email.
- Ability to hide favourites and emoji reactions in the API with `[:instance, :show_reactions]` config.
<details>
<summary>API Changes</summary>
- Mastodon API: Add pleroma.parent_visible field to statuses.
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines.
- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`.
- Mastodon API: Support irreversible property for filters.
- Mastodon API: Add pleroma.favicon field to accounts.
- Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs
</details>
### Fixed
- Fix list pagination and other list issues.
- Support pagination in conversations API
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
- Rich Media Previews for Twitter links
- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
- Fix CSP policy generation to include remote Captcha services
- Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
- Emoji Packs could not be listed when instance was set to `public: false`
- Fix whole_word always returning false on filter get requests
- Migrations not working on OTP releases if the database was connected over ssl
- Fix relay following
## [2.0.7] - 2020-06-13 ## [2.0.7] - 2020-06-13
### Security ### Security
@ -107,6 +220,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2. Run database migrations (inside Pleroma directory): 2. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate` - OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate` - From Source: `mix ecto.migrate`
3. Reset status visibility counters (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl refresh_counter_cache`
- From Source: `mix pleroma.refresh_counter_cache`
## [2.0.2] - 2020-04-08 ## [2.0.2] - 2020-04-08
### Added ### Added
@ -128,6 +245,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Static-FE: Fix remote posts not being sanitized - Static-FE: Fix remote posts not being sanitized
### Fixed ### Fixed
=======
- Rate limiter crashes when there is no explicitly specified ip in the config - Rate limiter crashes when there is no explicitly specified ip in the config
- 500 errors when no `Accept` header is present if Static-FE is enabled - 500 errors when no `Accept` header is present if Static-FE is enabled
- Instance panel not being updated immediately due to wrong `Cache-Control` headers - Instance panel not being updated immediately due to wrong `Cache-Control` headers
@ -153,7 +271,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.0.0] - 2019-03-08 ## [2.0.0] - 2019-03-08
### Security ### Security
- Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request. - Mastodon API: Fix being able to request enormous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
### Removed ### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media` - **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
@ -162,6 +280,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking**: Using third party engines for user recommendation - **Breaking**: Using third party engines for user recommendation
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- **Breaking**: AdminAPI: migrate_from_db endpoint - **Breaking**: AdminAPI: migrate_from_db endpoint
</details> </details>
@ -197,7 +316,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` - **Breaking** replying to reports is now "report notes", endpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes`
- Mastodon API: stopped sanitizing display names, field names and subject fields since they are supposed to be treated as plaintext - Mastodon API: stopped sanitizing display names, field names and subject fields since they are supposed to be treated as plaintext
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)

View File

@ -1,4 +1,4 @@
Unless otherwise stated this repository is copyright © 2017-2019 Unless otherwise stated this repository is copyright © 2017-2020
Pleroma Authors <https://pleroma.social/>, and is distributed under Pleroma Authors <https://pleroma.social/>, and is distributed under
The GNU Affero General Public License Version 3, you should have received a The GNU Affero General Public License Version 3, you should have received a
copy of the license file as AGPL-3. copy of the license file as AGPL-3.
@ -23,7 +23,7 @@ priv/static/images/pleroma-fox-tan-shy.png
--- ---
The following files are copyright © 2017-2019 Pleroma Authors The following files are copyright © 2017-2020 Pleroma Authors
<https://pleroma.social/>, and are distributed under the Creative Commons <https://pleroma.social/>, and are distributed under the Creative Commons
Attribution-ShareAlike 4.0 International license, you should have received Attribution-ShareAlike 4.0 International license, you should have received
a copy of the license file as CC-BY-SA-4.0. a copy of the license file as CC-BY-SA-4.0.

View File

@ -4,7 +4,7 @@ COPY . .
ENV MIX_ENV=prod ENV MIX_ENV=prod
RUN apk add git gcc g++ musl-dev make &&\ RUN apk add git gcc g++ musl-dev make cmake &&\
echo "import Mix.Config" > config/prod.secret.exs &&\ echo "import Mix.Config" > config/prod.secret.exs &&\
mix local.hex --force &&\ mix local.hex --force &&\
mix local.rebar --force &&\ mix local.rebar --force &&\
@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make &&\
mkdir release &&\ mkdir release &&\
mix release --path release mix release --path release
FROM alpine:3.9 FROM alpine:3.11
ARG BUILD_DATE ARG BUILD_DATE
ARG VCS_REF ARG VCS_REF
@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma
RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\
apk update &&\ apk update &&\
apk add ncurses postgresql-client &&\ apk add exiftool imagemagick ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/uploads &&\
mkdir -p ${DATA}/static &&\ mkdir -p ${DATA}/static &&\

View File

@ -34,6 +34,16 @@ Currently Pleroma is not packaged by any OS/Distros, but if you want to package
### Docker ### Docker
While we dont provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>. While we dont provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
### Compilation Troubleshooting
If you ever encounter compilation issues during the updating of Pleroma, you can try these commands and see if they fix things:
- `mix deps.clean --all`
- `mix local.rebar`
- `mix local.hex`
- `rm -r _build`
If you are not developing Pleroma, it is better to use the OTP release, which comes with everything precompiled.
## Documentation ## Documentation
- Latest Released revision: <https://docs.pleroma.social> - Latest Released revision: <https://docs.pleroma.social>
- Latest Git revision: <https://docs-develop.pleroma.social> - Latest Git revision: <https://docs-develop.pleroma.social>

16
SECURITY.md Normal file
View File

@ -0,0 +1,16 @@
# Pleroma backend security policy
## Supported versions
Currently, Pleroma offers bugfixes and security patches only for the latest minor release.
| Version | Support
|---------| --------
| 2.0 | Bugfixes and security patches
## Reporting a vulnerability
Please use confidential issues (tick the "This issue is confidential and should only be visible to team members with at least Reporter access." box when submitting) at our [bugtracker](https://git.pleroma.social/pleroma/pleroma/-/issues/new) for reporting vulnerabilities.
## Announcements
New releases are announced at [pleroma.social](https://pleroma.social/announcements/). All security releases are tagged with ["Security"](https://pleroma.social/announcements/tags/security/). You can be notified of them by subscribing to an Atom feed at <https://pleroma.social/announcements/tags/security/feed.xml>.

View File

@ -0,0 +1,595 @@
defmodule Pleroma.LoadTesting.Activities do
@moduledoc """
Module for generating different activities.
"""
import Ecto.Query
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
alias Ecto.UUID
alias Pleroma.Constants
alias Pleroma.LoadTesting.Users
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI
require Constants
@defaults [
iterations: 170,
friends_used: 20,
non_friends_used: 20
]
@max_concurrency 10
@visibility ~w(public private direct unlisted)
@types [
:simple,
:simple_filtered,
:emoji,
:mentions,
:hell_thread,
:attachment,
:tag,
:like,
:reblog,
:simple_thread
]
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
@remote_groups [:friends_remote, :non_friends_remote]
@friends_groups [:friends_local, :friends_remote]
@non_friends_groups [:non_friends_local, :non_friends_remote]
@spec generate(User.t(), keyword()) :: :ok
def generate(user, opts \\ []) do
{:ok, _} =
Agent.start_link(fn -> %{} end,
name: :benchmark_state
)
opts = Keyword.merge(@defaults, opts)
users = Users.prepare_users(user, opts)
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
task_data =
for visibility <- @visibility,
type <- @types,
group <- [:user | @groups],
do: {visibility, type, group}
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
public_long_thread = fn ->
generate_long_thread("public", users, opts)
end
private_long_thread = fn ->
generate_long_thread("private", users, opts)
end
iterations = opts[:iterations]
{time, _} =
:timer.tc(fn ->
Enum.each(
1..iterations,
fn
i when i == iterations - 2 ->
spawn(public_long_thread)
spawn(private_long_thread)
generate_activities(users, Enum.shuffle(task_data), opts)
_ ->
generate_activities(users, Enum.shuffle(task_data), opts)
end
)
end)
IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n")
:ok
end
def generate_power_intervals(opts \\ []) do
count = Keyword.get(opts, :count, 20)
power = Keyword.get(opts, :power, 2)
IO.puts("Generating #{count} intervals for a power #{power} series...")
counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
sum = Enum.sum(counts)
densities =
Enum.map(counts, fn c ->
c / sum
end)
densities
|> Enum.reduce(0, fn density, acc ->
if acc == 0 do
[{0, density}]
else
[{_, lower} | _] = acc
[{lower, lower + density} | acc]
end
end)
|> Enum.reverse()
end
def generate_tagged_activities(opts \\ []) do
tag_count = Keyword.get(opts, :tag_count, 20)
users = Keyword.get(opts, :users, Repo.all(Pleroma.User))
activity_count = Keyword.get(opts, :count, 200_000)
intervals = generate_power_intervals(count: tag_count)
IO.puts(
"Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
)
Enum.each(1..activity_count, fn _ ->
random = :rand.uniform()
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
end)
end
defp generate_long_thread(visibility, users, _opts) do
group =
if visibility == "public",
do: :friends_local,
else: :user
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
{:ok, activity} =
CommonAPI.post(users[:user], %{
status: "Start of #{visibility} long thread",
visibility: visibility
})
Agent.update(:benchmark_state, fn state ->
key =
if visibility == "public",
do: :public_thread,
else: :private_thread
Map.put(state, key, activity)
end)
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
insert_replies_for_long_thread(tasks, visibility, users, acc)
IO.puts("Generating #{visibility} long thread ended\n")
end
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
user = users[:user]
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
end)
end
defp generate_activities(users, task_data, opts) do
Task.async_stream(
task_data,
fn {visibility, type, group} ->
insert_activity(type, visibility, group, users, opts)
end,
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Stream.run()
end
defp insert_local_activity(visibility, group, users, status) do
{:ok, _} =
group
|> get_actor(users)
|> CommonAPI.post(%{status: status, visibility: visibility})
end
defp insert_remote_activity(visibility, group, users, status) do
actor = get_actor(group, users)
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
{activity_data, object_data} = other_data(actor, status)
activity_data
|> Map.merge(act_data)
|> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
end
defp user_mentions(users) do
user_mentions =
Enum.reduce(
@groups,
[],
fn group, acc ->
acc ++ get_random_mentions(users[group], Enum.random(0..2))
end
)
if Enum.random([true, false]),
do: ["@" <> users[:user].nickname | user_mentions],
else: user_mentions
end
defp hell_thread_mentions(users) do
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
cached =
@groups
|> Enum.reduce([users[:user]], fn group, acc ->
acc ++ Enum.take(users[group], 5)
end)
|> Enum.map(&"@#{&1.nickname}")
|> Enum.join(", ")
Cachex.put(:user_cache, "hell_thread_mentions", cached)
cached
else
{:ok, cached} -> cached
end
end
defp insert_activity(:simple, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status")
end
defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status")
end
defp insert_activity(:simple_filtered, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
end
defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status which must be filtered")
end
defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
end
defp insert_activity(:emoji, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
end
defp insert_activity(:mentions, visibility, group, users, _opts)
when group in @remote_groups do
mentions = user_mentions(users)
status = Enum.join(mentions, ", ") <> " remote status with mentions"
insert_remote_activity(visibility, group, users, status)
end
defp insert_activity(:mentions, visibility, group, users, _opts) do
mentions = user_mentions(users)
status = Enum.join(mentions, ", ") <> " simple status with mentions"
insert_remote_activity(visibility, group, users, status)
end
defp insert_activity(:hell_thread, visibility, group, users, _)
when group in @remote_groups do
mentions = hell_thread_mentions(users)
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
end
defp insert_activity(:hell_thread, visibility, group, users, _opts) do
mentions = hell_thread_mentions(users)
insert_local_activity(visibility, group, users, mentions <> " hell thread status")
end
defp insert_activity(:attachment, visibility, group, users, _opts) do
actor = get_actor(group, users)
obj_data = %{
"actor" => actor.ap_id,
"name" => "4467-11.jpg",
"type" => "Document",
"url" => [
%{
"href" =>
"#{Pleroma.Web.base_url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
"mediaType" => "image/jpeg",
"type" => "Link"
}
]
}
object = Repo.insert!(%Pleroma.Object{data: obj_data})
{:ok, _activity} =
CommonAPI.post(actor, %{
status: "Post with attachment",
visibility: visibility,
media_ids: [object.id]
})
end
defp insert_activity(:tag, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Status with #tag")
end
defp insert_activity(:like, visibility, group, users, opts) do
actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok
else
{:error, _} ->
insert_activity(:like, visibility, group, users, opts)
nil ->
Process.sleep(15)
insert_activity(:like, visibility, group, users, opts)
end
end
defp insert_activity(:reblog, visibility, group, users, opts) do
actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
:ok
else
{:error, _} ->
insert_activity(:reblog, visibility, group, users, opts)
nil ->
Process.sleep(15)
insert_activity(:reblog, visibility, group, users, opts)
end
end
defp insert_activity(:simple_thread, "direct", group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks("direct", group)
list =
case group do
:user ->
group = Enum.random(@friends_groups)
Enum.take(users[group], 3)
_ ->
Enum.take(users[group], 3)
end
data = Enum.map(list, &("@" <> &1.nickname))
{:ok, activity} =
CommonAPI.post(actor, %{
status: Enum.join(data, ", ") <> "simple status",
visibility: "direct"
})
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
insert_direct_replies(tasks, users[:user], list, acc)
end
defp insert_activity(:simple_thread, visibility, group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks(visibility, group)
{:ok, activity} =
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, users, acc)
end
defp get_actor(:user, %{user: user}), do: user
defp get_actor(group, users), do: Enum.random(users[group])
defp other_data(actor, content) do
%{host: host} = URI.parse(actor.ap_id)
datetime = DateTime.utc_now()
context_id = "https://#{host}/contexts/#{UUID.generate()}"
activity_id = "https://#{host}/activities/#{UUID.generate()}"
object_id = "https://#{host}/objects/#{UUID.generate()}"
activity_data = %{
"actor" => actor.ap_id,
"context" => context_id,
"id" => activity_id,
"published" => datetime,
"type" => "Create",
"directMessage" => false
}
object_data = %{
"actor" => actor.ap_id,
"attachment" => [],
"attributedTo" => actor.ap_id,
"bcc" => [],
"bto" => [],
"content" => content,
"context" => context_id,
"conversation" => context_id,
"emoji" => %{},
"id" => object_id,
"published" => datetime,
"sensitive" => false,
"summary" => "",
"tag" => [],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Note"
}
{activity_data, object_data}
end
defp prepare_activity_data(actor, "public", _mention) do
obj_data = %{
"cc" => [actor.follower_address],
"to" => [Constants.as_public()]
}
act_data = %{
"cc" => [actor.follower_address],
"to" => [Constants.as_public()]
}
{act_data, obj_data}
end
defp prepare_activity_data(actor, "private", _mention) do
obj_data = %{
"cc" => [],
"to" => [actor.follower_address]
}
act_data = %{
"cc" => [],
"to" => [actor.follower_address]
}
{act_data, obj_data}
end
defp prepare_activity_data(actor, "unlisted", _mention) do
obj_data = %{
"cc" => [Constants.as_public()],
"to" => [actor.follower_address]
}
act_data = %{
"cc" => [Constants.as_public()],
"to" => [actor.follower_address]
}
{act_data, obj_data}
end
defp prepare_activity_data(_actor, "direct", mention) do
%{host: mentioned_host} = URI.parse(mention.ap_id)
obj_data = %{
"cc" => [],
"content" =>
"<span class=\"h-card\"><a class=\"u-url mention\" href=\"#{mention.ap_id}\" rel=\"ugc\">@<span>#{
mention.nickname
}</span></a></span> direct message",
"tag" => [
%{
"href" => mention.ap_id,
"name" => "@#{mention.nickname}@#{mentioned_host}",
"type" => "Mention"
}
],
"to" => [mention.ap_id]
}
act_data = %{
"cc" => [],
"directMessage" => true,
"to" => [mention.ap_id]
}
{act_data, obj_data}
end
defp get_reply_tasks("public", :user) do
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
end
defp get_reply_tasks("public", group) when group in @friends_groups do
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks("public", group) when group in @non_friends_groups do
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
end
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and group in @friends_groups do
[:user, :friends_remote, :friends_local, :user]
end
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and
group in @non_friends_groups,
do: []
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
defp get_reply_tasks("direct", group) when group in @friends_groups,
do: [:user, group, :user]
defp get_reply_tasks("direct", group) when group in @non_friends_groups do
[:user, :non_friends_remote, :user, :non_friends_local]
end
defp insert_replies(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
insert_reply(users[:user], data, id, visibility)
group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, data, id, visibility)
end)
end
defp insert_direct_replies(tasks, user, list, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}
_, {id, data} ->
actor = Enum.random(list)
{reply_id, _} =
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
{reply_id, data}
end)
end
defp insert_reply(actor, data, activity_id, visibility) do
{:ok, reply} =
CommonAPI.post(actor, %{
status: Enum.join(data, ", "),
visibility: visibility,
in_reply_to_status_id: activity_id
})
{reply.id, ["@" <> actor.nickname | data]}
end
defp get_random_mentions(_users, count) when count == 0, do: []
defp get_random_mentions(users, count) do
users
|> Enum.shuffle()
|> Enum.take(count)
|> Enum.map(&"@#{&1.nickname}")
end
defp get_random_create_activity_id do
Repo.one(
from(a in Pleroma.Activity,
where: fragment("(?)->>'type' = ?", a.data, ^"Create"),
order_by: fragment("RANDOM()"),
limit: 1,
select: a.id
)
)
end
end

View File

@ -1,260 +1,623 @@
defmodule Pleroma.LoadTesting.Fetcher do defmodule Pleroma.LoadTesting.Fetcher do
use Pleroma.LoadTesting.Helper alias Pleroma.Activity
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView
def fetch_user(user) do @spec run_benchmarks(User.t()) :: any()
Benchee.run(%{ def run_benchmarks(user) do
fetch_user(user)
fetch_timelines(user)
render_views(user)
end
defp formatters do
[
Benchee.Formatters.Console
]
end
defp fetch_user(user) do
Benchee.run(
%{
"By id" => fn -> Repo.get_by(User, id: user.id) end, "By id" => fn -> Repo.get_by(User, id: user.id) end,
"By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end, "By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end,
"By email" => fn -> Repo.get_by(User, email: user.email) end, "By email" => fn -> Repo.get_by(User, email: user.email) end,
"By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end "By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end
},
formatters: formatters()
)
end
defp create_filter(user) do
Pleroma.Filter.create(%Pleroma.Filter{
user_id: user.id,
phrase: "must be filtered",
hide: true
}) })
end end
def query_timelines(user) do defp delete_filter(filter), do: Repo.delete(filter)
home_timeline_params = %{
"count" => 20,
"with_muted" => true,
"type" => ["Create", "Announce"],
"blocking_user" => user,
"muting_user" => user,
"user" => user
}
mastodon_public_timeline_params = %{ defp fetch_timelines(user) do
"count" => 20, fetch_home_timeline(user)
"local_only" => true, fetch_home_timeline_with_filter(user)
"only_media" => "false", fetch_direct_timeline(user)
"type" => ["Create", "Announce"], fetch_public_timeline(user)
"with_muted" => "true", fetch_public_timeline_with_filter(user)
"blocking_user" => user, fetch_public_timeline(user, :with_blocks)
"muting_user" => user fetch_public_timeline(user, :local)
} fetch_public_timeline(user, :tag)
fetch_notifications(user)
fetch_favourites(user)
fetch_long_thread(user)
fetch_timelines_with_reply_filtering(user)
end
mastodon_federated_timeline_params = %{ defp render_views(user) do
"count" => 20, render_timelines(user)
"only_media" => "false", render_long_thread(user)
"type" => ["Create", "Announce"], end
"with_muted" => "true",
"blocking_user" => user,
"muting_user" => user
}
following = User.following(user) defp opts_for_home_timeline(user) do
%{
blocking_user: user,
count: "20",
muting_user: user,
type: ["Create", "Announce"],
user: user,
with_muted: true
}
end
defp fetch_home_timeline(user, title_end \\ "") do
opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
first_page_last =
ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last()
second_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id))
|> Enum.reverse()
|> List.last()
third_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id))
|> Enum.reverse()
|> List.last()
forth_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id))
|> Enum.reverse()
|> List.last()
title = "home timeline " <> title_end
Benchee.run(
%{
title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id),
"1 page only media" => Map.put(opts, :only_media, true),
"2 page only media" =>
Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true),
"3 page only media" =>
Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true),
"4 page only media" =>
Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true),
"5 page only media" =>
Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true)
},
formatters: formatters()
)
end
defp fetch_home_timeline_with_filter(user) do
{:ok, filter} = create_filter(user)
fetch_home_timeline(user, "with filters")
delete_filter(filter)
end
defp opts_for_direct_timeline(user) do
%{
visibility: "direct",
blocking_user: user,
count: "20",
type: "Create",
user: user,
with_muted: true
}
end
defp fetch_direct_timeline(user) do
recipients = [user.ap_id]
opts = opts_for_direct_timeline(user)
first_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts)
|> Pagination.fetch_paginated(opts)
|> List.last()
opts2 = Map.put(opts, :max_id, first_page_last.id)
second_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts2)
|> Pagination.fetch_paginated(opts2)
|> List.last()
opts3 = Map.put(opts, :max_id, second_page_last.id)
third_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts3)
|> Pagination.fetch_paginated(opts3)
|> List.last()
opts4 = Map.put(opts, :max_id, third_page_last.id)
forth_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts4)
|> Pagination.fetch_paginated(opts4)
|> List.last()
Benchee.run(
%{
"direct timeline" => fn opts ->
ActivityPub.fetch_activities_query(recipients, opts) |> Pagination.fetch_paginated(opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => opts2,
"3 page" => opts3,
"4 page" => opts4,
"5 page" => Map.put(opts4, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp opts_for_public_timeline(user) do
%{
type: ["Create", "Announce"],
local_only: false,
blocking_user: user,
muting_user: user
}
end
defp opts_for_public_timeline(user, :local) do
%{
type: ["Create", "Announce"],
local_only: true,
blocking_user: user,
muting_user: user
}
end
defp opts_for_public_timeline(user, :tag) do
%{
blocking_user: user,
count: "20",
local_only: nil,
muting_user: user,
tag: ["tag"],
tag_all: [],
tag_reject: [],
type: "Create",
user: user,
with_muted: true
}
end
defp fetch_public_timeline(user) do
opts = opts_for_public_timeline(user)
fetch_public_timeline(opts, "public timeline")
end
defp fetch_public_timeline_with_filter(user) do
{:ok, filter} = create_filter(user)
opts = opts_for_public_timeline(user)
fetch_public_timeline(opts, "public timeline with filters")
delete_filter(filter)
end
defp fetch_public_timeline(user, :local) do
opts = opts_for_public_timeline(user, :local)
fetch_public_timeline(opts, "public timeline only local")
end
defp fetch_public_timeline(user, :tag) do
opts = opts_for_public_timeline(user, :tag)
fetch_public_timeline(opts, "hashtag timeline")
end
defp fetch_public_timeline(user, :only_media) do
opts = opts_for_public_timeline(user) |> Map.put(:only_media, true)
fetch_public_timeline(opts, "public timeline only media")
end
defp fetch_public_timeline(user, :with_blocks) do
opts = opts_for_public_timeline(user)
remote_non_friends = Agent.get(:non_friends_remote, & &1)
Benchee.run(%{ Benchee.run(%{
"User home timeline" => fn -> "public timeline without blocks" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( ActivityPub.fetch_public_activities(opts)
following,
home_timeline_params
)
end,
"User mastodon public timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_public_timeline_params
)
end,
"User mastodon federated public timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_federated_timeline_params
)
end end
}) })
home_activities = Enum.each(remote_non_friends, fn non_friend ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( {:ok, _} = User.block(user, non_friend)
following, end)
home_timeline_params
)
public_activities = user = User.get_by_id(user.id)
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params)
public_federated_activities = opts = Map.put(opts, :blocking_user, user)
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_federated_timeline_params
)
Benchee.run(%{ Benchee.run(%{
"public timeline with user block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
domains =
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
{:ok, _user} = User.unblock(user, non_friend)
%{host: host} = URI.parse(non_friend.ap_id)
[host | domains]
end)
domains = Enum.uniq(domains)
Enum.each(domains, fn domain ->
{:ok, _} = User.block_domain(user, domain)
end)
user = User.get_by_id(user.id)
opts = Map.put(opts, :blocking_user, user)
Benchee.run(%{
"public timeline with domain block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
end
defp fetch_public_timeline(opts, title) when is_binary(title) do
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()
second_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id))
|> List.last()
third_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id))
|> List.last()
forth_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id))
|> List.last()
Benchee.run(
%{
title => fn opts ->
ActivityPub.fetch_public_activities(opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp opts_for_notifications do
%{count: "20", with_muted: true}
end
defp fetch_notifications(user) do
opts = opts_for_notifications()
first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last()
second_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id))
|> List.last()
third_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id))
|> List.last()
forth_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id))
|> List.last()
Benchee.run(
%{
"Notifications" => fn opts ->
MastodonAPI.get_notifications(user, opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp fetch_favourites(user) do
first_page_last = ActivityPub.fetch_favourites(user) |> List.last()
second_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last()
third_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last()
forth_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last()
Benchee.run(
%{
"Favourites" => fn opts ->
ActivityPub.fetch_favourites(user, opts)
end
},
inputs: %{
"1 page" => %{},
"2 page" => %{:max_id => first_page_last.id},
"3 page" => %{:max_id => second_page_last.id},
"4 page" => %{:max_id => third_page_last.id},
"5 page" => %{:max_id => forth_page_last.id}
},
formatters: formatters()
)
end
defp opts_for_long_thread(user) do
%{
blocking_user: user,
user: user
}
end
defp fetch_long_thread(user) do
%{public_thread: public, private_thread: private} =
Agent.get(:benchmark_state, fn state -> state end)
opts = opts_for_long_thread(user)
private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)}
public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)}
Benchee.run(
%{
"fetch context" => fn {context, opts} ->
ActivityPub.fetch_activities_for_context(context, opts)
end
},
inputs: %{
"Private long thread" => private_input,
"Public long thread" => public_input
},
formatters: formatters()
)
end
defp render_timelines(user) do
opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
home_activities = ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse()
recipients = [user.ap_id]
opts = opts_for_direct_timeline(user)
direct_activities =
recipients
|> ActivityPub.fetch_activities_query(opts)
|> Pagination.fetch_paginated(opts)
opts = opts_for_public_timeline(user)
public_activities = ActivityPub.fetch_public_activities(opts)
opts = opts_for_public_timeline(user, :tag)
tag_activities = ActivityPub.fetch_public_activities(opts)
opts = opts_for_notifications()
notifications = MastodonAPI.get_notifications(user, opts)
favourites = ActivityPub.fetch_favourites(user)
Benchee.run(
%{
"Rendering home timeline" => fn -> "Rendering home timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: home_activities, activities: home_activities,
for: user, for: user,
as: :activity as: :activity
}) })
end, end,
"Rendering direct timeline" => fn ->
StatusView.render("index.json", %{
activities: direct_activities,
for: user,
as: :activity
})
end,
"Rendering public timeline" => fn -> "Rendering public timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: public_activities, activities: public_activities,
for: user, for: user,
as: :activity as: :activity
}) })
end, end,
"Rendering public federated timeline" => fn -> "Rendering tag timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: public_federated_activities, activities: tag_activities,
for: user, for: user,
as: :activity as: :activity
}) })
end, end,
"Rendering favorites timeline" => fn -> "Rendering notifications" => fn ->
conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil) Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
Pleroma.Web.MastodonAPI.StatusController.favourites( notifications: notifications,
%Plug.Conn{conn | for: user
assigns: %{user: user}, })
query_params: %{"limit" => "0"}, end,
body_params: %{}, "Rendering favourites timeline" => fn ->
cookies: %{}, StatusView.render("index.json", %{
params: %{}, activities: favourites,
path_params: %{}, for: user,
private: %{ as: :activity
Pleroma.Web.Router => {[], %{}}, })
phoenix_router: Pleroma.Web.Router, end
phoenix_action: :favourites, },
phoenix_controller: Pleroma.Web.MastodonAPI.StatusController, formatters: formatters()
phoenix_endpoint: Pleroma.Web.Endpoint, )
phoenix_format: "json", end
phoenix_layout: {Pleroma.Web.LayoutView, "app.html"},
phoenix_recycled: true,
phoenix_view: Pleroma.Web.MastodonAPI.StatusView, defp render_long_thread(user) do
plug_session: %{"user_id" => user.id}, %{public_thread: public, private_thread: private} =
plug_session_fetch: :done, Agent.get(:benchmark_state, fn state -> state end)
plug_session_info: :write,
plug_skip_csrf_protection: true opts = %{for: user}
public_activity = Activity.get_by_id_with_object(public.id)
private_activity = Activity.get_by_id_with_object(private.id)
Benchee.run(
%{
"render" => fn opts ->
StatusView.render("show.json", opts)
end
},
inputs: %{
"Public root" => Map.put(opts, :activity, public_activity),
"Private root" => Map.put(opts, :activity, private_activity)
},
formatters: formatters()
)
fetch_opts = opts_for_long_thread(user)
public_context =
ActivityPub.fetch_activities_for_context(
public.data["context"],
Map.put(fetch_opts, :exclude_id, public.id)
)
private_context =
ActivityPub.fetch_activities_for_context(
private.data["context"],
Map.put(fetch_opts, :exclude_id, private.id)
)
Benchee.run(
%{
"render" => fn opts ->
StatusView.render("context.json", opts)
end
},
inputs: %{
"Public context" => %{user: user, activity: public_activity, activities: public_context},
"Private context" => %{
user: user,
activity: private_activity,
activities: private_context
} }
}, },
%{}) formatters: formatters()
end, )
})
end end
def query_notifications(user) do defp fetch_timelines_with_reply_filtering(user) do
without_muted_params = %{"count" => "20", "with_muted" => "false"} public_params = opts_for_public_timeline(user)
with_muted_params = %{"count" => "20", "with_muted" => "true"}
Benchee.run(%{ Benchee.run(
"Notifications without muted" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
end,
"Notifications with muted" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
end
})
without_muted_notifications =
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
with_muted_notifications =
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
Benchee.run(%{
"Render notifications without muted" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: without_muted_notifications,
for: user
})
end,
"Render notifications with muted" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: with_muted_notifications,
for: user
})
end
})
end
def query_dms(user) do
params = %{
"count" => "20",
"with_muted" => "true",
"type" => "Create",
"blocking_user" => user,
"user" => user,
visibility: "direct"
}
Benchee.run(%{
"Direct messages with muted" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(params)
end,
"Direct messages without muted" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
end
})
dms_with_muted =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(params)
dms_without_muted =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
Benchee.run(%{
"Rendering dms with muted" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: dms_with_muted,
for: user,
as: :activity
})
end,
"Rendering dms without muted" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: dms_without_muted,
for: user,
as: :activity
})
end
})
end
def query_long_thread(user, activity) do
Benchee.run(%{
"Fetch main post" => fn ->
Pleroma.Activity.get_by_id_with_object(activity.id)
end,
"Fetch context of main post" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
activity.data["context"],
%{ %{
"blocking_user" => user, "Public timeline without reply filtering" => fn ->
"user" => user, ActivityPub.fetch_public_activities(public_params)
"exclude_id" => activity.id
}
)
end
})
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
context =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
activity.data["context"],
%{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
}
)
Benchee.run(%{
"Render status" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{
activity: activity,
for: user
})
end, end,
"Render context" => fn -> "Public timeline with reply filtering - following" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render( public_params
"index.json", |> Map.put(:reply_visibility, "following")
for: user, |> Map.put(:reply_filtering_user, user)
activities: context, |> ActivityPub.fetch_public_activities()
as: :activity end,
"Public timeline with reply filtering - self" => fn ->
public_params
|> Map.put(:reply_visibility, "self")
|> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities()
end
},
formatters: formatters()
) )
|> Enum.reverse()
private_params = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
Benchee.run(
%{
"Home timeline without reply filtering" => fn ->
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - following" => fn ->
private_params =
private_params
|> Map.put(:reply_filtering_user, user)
|> Map.put(:reply_visibility, "following")
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - self" => fn ->
private_params =
private_params
|> Map.put(:reply_filtering_user, user)
|> Map.put(:reply_visibility, "self")
ActivityPub.fetch_activities(recipients, private_params)
end end
}) },
formatters: formatters()
)
end end
end end

View File

@ -1,409 +0,0 @@
defmodule Pleroma.LoadTesting.Generator do
use Pleroma.LoadTesting.Helper
alias Pleroma.Web.CommonAPI
def generate_like_activities(user, posts) do
count_likes = Kernel.trunc(length(posts) / 4)
IO.puts("Starting generating #{count_likes} like activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
Enum.take_random(posts, count_likes),
fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting like activities take #{to_sec(time)} sec.\n")
end
def generate_users(opts) do
IO.puts("Starting generating #{opts[:users_max]} users...")
{time, _} = :timer.tc(fn -> do_generate_users(opts) end)
IO.puts("Inserting users take #{to_sec(time)} sec.\n")
end
defp do_generate_users(opts) do
max = Keyword.get(opts, :users_max)
Task.async_stream(
1..max,
&generate_user_data(&1),
max_concurrency: 10,
timeout: 30_000
)
|> Enum.to_list()
end
defp generate_user_data(i) do
remote = Enum.random([true, false])
user = %User{
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
password_hash:
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
bio: "Tester Number #{i}",
local: remote
}
user_urls =
if remote do
base_url =
Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"])
ap_id = "#{base_url}/users/#{user.nickname}"
%{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
end
user = Map.merge(user, user_urls)
Repo.insert!(user)
end
def generate_activities(user, users) do
do_generate_activities(user, users)
end
defp do_generate_activities(user, users) do
IO.puts("Starting generating 20000 common activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..20_000,
fn _ ->
do_generate_activity([user | users])
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting common activities take #{to_sec(time)} sec.\n")
IO.puts("Starting generating 20000 activities with mentions...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..20_000,
fn _ ->
do_generate_activity_with_mention(user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n")
IO.puts("Starting generating 10000 activities with threads...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..10_000,
fn _ ->
do_generate_threads([user | users])
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n")
end
defp do_generate_activity(users) do
post = %{
"status" => "Some status without mention with random user"
}
CommonAPI.post(Enum.random(users), post)
end
def generate_power_intervals(opts \\ []) do
count = Keyword.get(opts, :count, 20)
power = Keyword.get(opts, :power, 2)
IO.puts("Generating #{count} intervals for a power #{power} series...")
counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
sum = Enum.sum(counts)
densities =
Enum.map(counts, fn c ->
c / sum
end)
densities
|> Enum.reduce(0, fn density, acc ->
if acc == 0 do
[{0, density}]
else
[{_, lower} | _] = acc
[{lower, lower + density} | acc]
end
end)
|> Enum.reverse()
end
def generate_tagged_activities(opts \\ []) do
tag_count = Keyword.get(opts, :tag_count, 20)
users = Keyword.get(opts, :users, Repo.all(User))
activity_count = Keyword.get(opts, :count, 200_000)
intervals = generate_power_intervals(count: tag_count)
IO.puts(
"Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
)
Enum.each(1..activity_count, fn _ ->
random = :rand.uniform()
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"})
end)
end
defp do_generate_activity_with_mention(user, users) do
mentions_cnt = Enum.random([2, 3, 4, 5])
with_user = Enum.random([true, false])
users = Enum.shuffle(users)
mentions_users = Enum.take(users, mentions_cnt)
mentions_users = if with_user, do: [user | mentions_users], else: mentions_users
mentions_str =
Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ")
post = %{
"status" => mentions_str <> "some status with mentions random users"
}
CommonAPI.post(Enum.random(users), post)
end
defp do_generate_threads(users) do
thread_length = Enum.random([2, 3, 4, 5])
actor = Enum.random(users)
post = %{
"status" => "Start of the thread"
}
{:ok, activity} = CommonAPI.post(actor, post)
Enum.each(1..thread_length, fn _ ->
user = Enum.random(users)
post = %{
"status" => "@#{actor.nickname} reply to thread",
"in_reply_to_status_id" => activity.id
}
CommonAPI.post(user, post)
end)
end
def generate_remote_activities(user, users) do
do_generate_remote_activities(user, users)
end
defp do_generate_remote_activities(user, users) do
IO.puts("Starting generating 10000 remote activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..10_000,
fn i ->
do_generate_remote_activity(i, user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n")
end
defp do_generate_remote_activity(i, user, users) do
actor = Enum.random(users)
%{host: host} = URI.parse(actor.ap_id)
date = Date.utc_today()
datetime = DateTime.utc_now()
map = %{
"actor" => actor.ap_id,
"cc" => [actor.follower_address, user.ap_id],
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"id" => actor.ap_id <> "/statuses/#{i}/activity",
"object" => %{
"actor" => actor.ap_id,
"atomUri" => actor.ap_id <> "/statuses/#{i}",
"attachment" => [],
"attributedTo" => actor.ap_id,
"bcc" => [],
"bto" => [],
"cc" => [actor.follower_address, user.ap_id],
"content" =>
"<p><span class=\"h-card\"><a href=\"" <>
user.ap_id <>
"\" class=\"u-url mention\">@<span>" <> user.nickname <> "</span></a></span></p>",
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"conversation" =>
"tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"emoji" => %{},
"id" => actor.ap_id <> "/statuses/#{i}",
"inReplyTo" => nil,
"inReplyToAtomUri" => nil,
"published" => datetime,
"sensitive" => true,
"summary" => "cw",
"tag" => [
%{
"href" => user.ap_id,
"name" => "@#{user.nickname}@#{host}",
"type" => "Mention"
}
],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Note",
"url" => "http://#{host}/@#{actor.nickname}/#{i}"
},
"published" => datetime,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create"
}
Pleroma.Web.ActivityPub.ActivityPub.insert(map, false)
end
def generate_dms(user, users, opts) do
IO.puts("Starting generating #{opts[:dms_max]} DMs")
{time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end)
IO.puts("Inserting dms take #{to_sec(time)} sec.\n")
end
defp do_generate_dms(user, users, opts) do
Task.async_stream(
1..opts[:dms_max],
fn _ ->
do_generate_dm(user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end
defp do_generate_dm(user, users) do
post = %{
"status" => "@#{user.nickname} some direct message",
"visibility" => "direct"
}
CommonAPI.post(Enum.random(users), post)
end
def generate_long_thread(user, users, opts) do
IO.puts("Starting generating long thread with #{opts[:thread_length]} replies")
{time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end)
IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n")
{:ok, activity}
end
defp do_generate_long_thread(user, users, opts) do
{:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"})
Task.async_stream(
1..opts[:thread_length],
fn _ -> do_generate_thread(users, id) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
activity
end
defp do_generate_thread(users, activity_id) do
CommonAPI.post(Enum.random(users), %{
"status" => "reply to main post",
"in_reply_to_status_id" => activity_id
})
end
def generate_non_visible_message(user, users) do
IO.puts("Starting generating 1000 non visible posts")
{time, _} =
:timer.tc(fn ->
do_generate_non_visible_posts(user, users)
end)
IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n")
end
defp do_generate_non_visible_posts(user, users) do
[not_friend | users] = users
make_friends(user, users)
Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end
defp make_friends(_user, []), do: nil
defp make_friends(user, [friend | users]) do
{:ok, _} = User.follow(user, friend)
{:ok, _} = User.follow(friend, user)
make_friends(user, users)
end
defp do_generate_non_visible_post(not_friend, users) do
post = %{
"status" => "some non visible post",
"visibility" => "private"
}
{:ok, activity} = CommonAPI.post(not_friend, post)
thread_length = Enum.random([2, 3, 4, 5])
Enum.each(1..thread_length, fn _ ->
user = Enum.random(users)
post = %{
"status" => "@#{not_friend.nickname} reply to non visible post",
"in_reply_to_status_id" => activity.id,
"visibility" => "private"
}
CommonAPI.post(user, post)
end)
end
end

View File

@ -1,11 +1,14 @@
defmodule Pleroma.LoadTesting.Helper do defmodule Pleroma.LoadTesting.Helper do
defmacro __using__(_) do alias Ecto.Adapters.SQL
quote do
import Ecto.Query
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
defp to_sec(microseconds), do: microseconds / 1_000_000 def to_sec(microseconds), do: microseconds / 1_000_000
end
def clean_tables do
IO.puts("Deleting old data...\n")
SQL.query!(Repo, "TRUNCATE users CASCADE;")
SQL.query!(Repo, "TRUNCATE activities CASCADE;")
SQL.query!(Repo, "TRUNCATE objects CASCADE;")
SQL.query!(Repo, "TRUNCATE oban_jobs CASCADE;")
end end
end end

View File

@ -0,0 +1,189 @@
defmodule Pleroma.LoadTesting.Users do
@moduledoc """
Module for generating users with friends.
"""
import Ecto.Query
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.User.Query
@defaults [
users: 20_000,
friends: 100
]
@max_concurrency 10
@spec generate(keyword()) :: User.t()
def generate(opts \\ []) do
opts = Keyword.merge(@defaults, opts)
generate_users(opts[:users])
main_user =
Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1))
make_friends(main_user, opts[:friends])
User.get_by_id(main_user.id)
end
def generate_users(max) do
IO.puts("Starting generating #{max} users...")
{time, users} =
:timer.tc(fn ->
Task.async_stream(
1..max,
&generate_user(&1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Enum.to_list()
end)
IO.puts("Generating users took #{to_sec(time)} sec.\n")
users
end
defp generate_user(i) do
remote = Enum.random([true, false])
%User{
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
password_hash: Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}",
local: !remote
}
|> user_urls()
|> Repo.insert!()
end
defp user_urls(%{local: true} = user) do
urls = %{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
Map.merge(user, urls)
end
defp user_urls(%{local: false} = user) do
base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])
ap_id = "https://#{base_domain}/users/#{user.nickname}"
urls = %{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
}
Map.merge(user, urls)
end
def make_friends(main_user, max) when is_integer(max) do
IO.puts("Starting making friends for #{max} users...")
{time, _} =
:timer.tc(fn ->
number_of_users =
(max / 2)
|> Kernel.trunc()
main_user
|> get_users(%{limit: number_of_users, local: :local})
|> run_stream(main_user)
main_user
|> get_users(%{limit: number_of_users, local: :external})
|> run_stream(main_user)
end)
IO.puts("Making friends took #{to_sec(time)} sec.\n")
end
def make_friends(%User{} = main_user, %User{} = user) do
{:ok, _} = User.follow(main_user, user)
{:ok, _} = User.follow(user, main_user)
end
@spec get_users(User.t(), keyword()) :: [User.t()]
def get_users(user, opts) do
criteria = %{limit: opts[:limit]}
criteria =
if opts[:local] do
Map.put(criteria, opts[:local], true)
else
criteria
end
criteria =
if opts[:friends?] do
Map.put(criteria, :friends, user)
else
criteria
end
query =
criteria
|> Query.build()
|> random_without_user(user)
query =
if opts[:friends?] == false do
friends_ids =
%{friends: user}
|> Query.build()
|> Repo.all()
|> Enum.map(& &1.id)
from(u in query, where: u.id not in ^friends_ids)
else
query
end
Repo.all(query)
end
defp random_without_user(query, user) do
from(u in query,
where: u.id != ^user.id,
order_by: fragment("RANDOM()")
)
end
defp run_stream(users, main_user) do
Task.async_stream(users, &make_friends(main_user, &1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Stream.run()
end
@spec prepare_users(User.t(), keyword()) :: map()
def prepare_users(user, opts) do
friends_limit = opts[:friends_used]
non_friends_limit = opts[:non_friends_used]
%{
user: user,
friends_local: fetch_users(user, friends_limit, :local, true),
friends_remote: fetch_users(user, friends_limit, :external, true),
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
}
end
defp fetch_users(user, limit, local, friends?) do
user
|> get_users(limit: limit, local: local, friends?: friends?)
|> Enum.shuffle()
end
end

View File

@ -1,9 +1,11 @@
defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
use Mix.Task use Mix.Task
alias Pleroma.Repo
alias Pleroma.LoadTesting.Generator import Pleroma.LoadTesting.Helper, only: [clean_tables: 0]
import Ecto.Query import Ecto.Query
alias Pleroma.Repo
def run(_args) do def run(_args) do
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()
activities_count = Repo.aggregate(from(a in Pleroma.Activity), :count, :id) activities_count = Repo.aggregate(from(a in Pleroma.Activity), :count, :id)
@ -11,8 +13,8 @@ def run(_args) do
if activities_count == 0 do if activities_count == 0 do
IO.puts("Did not find any activities, cleaning and generating") IO.puts("Did not find any activities, cleaning and generating")
clean_tables() clean_tables()
Generator.generate_users(users_max: 10) Pleroma.LoadTesting.Users.generate_users(10)
Generator.generate_tagged_activities() Pleroma.LoadTesting.Activities.generate_tagged_activities()
else else
IO.puts("Found #{activities_count} activities, won't generate new ones") IO.puts("Found #{activities_count} activities, won't generate new ones")
end end
@ -34,7 +36,7 @@ def run(_args) do
Benchee.run( Benchee.run(
%{ %{
"Hashtag fetching, any" => fn tags -> "Hashtag fetching, any" => fn tags ->
Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"any" => tags "any" => tags
}, },
@ -44,7 +46,7 @@ def run(_args) do
end, end,
# Will always return zero results because no overlapping hashtags are generated. # Will always return zero results because no overlapping hashtags are generated.
"Hashtag fetching, all" => fn tags -> "Hashtag fetching, all" => fn tags ->
Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"all" => tags "all" => tags
}, },
@ -64,7 +66,7 @@ def run(_args) do
Benchee.run( Benchee.run(
%{ %{
"Hashtag fetching" => fn tag -> "Hashtag fetching" => fn tag ->
Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"tag" => tag "tag" => tag
}, },
@ -78,10 +80,34 @@ def run(_args) do
) )
end end
defp clean_tables do defp hashtag_fetching(params, user, local_only) do
IO.puts("Deleting old data...\n") tags =
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") [params["tag"], params["any"]]
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") |> List.flatten()
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") |> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))
tag_all =
params
|> Map.get("all", [])
|> Enum.map(&String.downcase(&1))
tag_reject =
params
|> Map.get("none", [])
|> Enum.map(&String.downcase(&1))
_activities =
params
|> Map.put(:type, "Create")
|> Map.put(:local_only, local_only)
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:user, user)
|> Map.put(:tag, tags)
|> Map.put(:tag_all, tag_all)
|> Map.put(:tag_reject, tag_reject)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end end
end end

View File

@ -0,0 +1,70 @@
defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do
use Mix.Task
import Pleroma.LoadTesting.Helper, only: [clean_tables: 0]
alias Pleroma.Web.CommonAPI
alias Plug.Conn
def run(_args) do
Mix.Pleroma.start_pleroma()
# Cleaning tables
clean_tables()
[{:ok, user} | users] = Pleroma.LoadTesting.Users.generate_users(1000)
# Let the user make 100 posts
1..100
|> Enum.each(fn i -> CommonAPI.post(user, %{"status" => to_string(i)}) end)
# Let 10 random users post
posts =
users
|> Enum.take_random(10)
|> Enum.map(fn {:ok, random_user} ->
{:ok, activity} = CommonAPI.post(random_user, %{"status" => "."})
activity
end)
# let our user repeat them
posts
|> Enum.each(fn activity ->
CommonAPI.repeat(activity.id, user)
end)
Benchee.run(
%{
"user timeline, no followers" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Conn.assign(:user, reading_user)
|> Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
users
|> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end)
Benchee.run(
%{
"user timeline, all following" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Conn.assign(:user, reading_user)
|> Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
end
end

View File

@ -1,114 +1,56 @@
defmodule Mix.Tasks.Pleroma.LoadTesting do defmodule Mix.Tasks.Pleroma.LoadTesting do
use Mix.Task use Mix.Task
use Pleroma.LoadTesting.Helper import Ecto.Query
import Mix.Pleroma import Pleroma.LoadTesting.Helper, only: [clean_tables: 0]
import Pleroma.LoadTesting.Generator
import Pleroma.LoadTesting.Fetcher alias Pleroma.Repo
alias Pleroma.User
@shortdoc "Factory for generation data" @shortdoc "Factory for generation data"
@moduledoc """ @moduledoc """
Generates data like: Generates data like:
- local/remote users - local/remote users
- local/remote activities with notifications - local/remote activities with differrent visibility:
- direct messages - simple activiities
- long thread - with emoji
- non visible posts - with mentions
- hellthreads
- with attachments
- with tags
- likes
- reblogs
- simple threads
- long threads
## Generate data ## Generate data
MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000 MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --friends 1000 --iterations 170 --friends_used 20 --non_friends_used 20
MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000 MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -f 1000 -i 170 -fu 20 -nfu 20
Options: Options:
- `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u` - `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u`
- `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d` - `--friends NUMBER` - number of friends for main user. Defaults to: 1000. Alias: `-f`
- `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t` - `--iterations NUMBER` - number of iterations to generate activities. For each iteration in database is inserted about 120+ activities with different visibility, actors and types.Defaults to: 170. Alias: `-i`
- `--friends_used NUMBER` - number of main user friends used in activity generation. Defaults to: 20. Alias: `-fu`
- `--non_friends_used NUMBER` - number of non friends used in activity generation. Defaults to: 20. Alias: `-nfu`
""" """
@aliases [u: :users, d: :dms, t: :thread_length] @aliases [u: :users, f: :friends, i: :iterations, fu: :friends_used, nfu: :non_friends_used]
@switches [ @switches [
users: :integer, users: :integer,
dms: :integer, friends: :integer,
thread_length: :integer iterations: :integer,
friends_used: :integer,
non_friends_used: :integer
] ]
@users_default 20_000
@dms_default 1_000
@thread_length_default 2_000
def run(args) do def run(args) do
start_pleroma() Logger.configure(level: :error)
Pleroma.Config.put([:instance, :skip_thread_containment], true) Mix.Pleroma.start_pleroma()
clean_tables()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
users_max = Keyword.get(opts, :users, @users_default) user = Pleroma.LoadTesting.Users.generate(opts)
dms_max = Keyword.get(opts, :dms, @dms_default) Pleroma.LoadTesting.Activities.generate(user, opts)
thread_length = Keyword.get(opts, :thread_length, @thread_length_default)
clean_tables()
opts =
Keyword.put(opts, :users_max, users_max)
|> Keyword.put(:dms_max, dms_max)
|> Keyword.put(:thread_length, thread_length)
generate_users(opts)
# main user for queries
IO.puts("Fetching local main user...")
{time, user} =
:timer.tc(fn ->
Repo.one(
from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)
)
end)
IO.puts("Fetching main user take #{to_sec(time)} sec.\n")
IO.puts("Fetching local users...")
{time, users} =
:timer.tc(fn ->
Repo.all(
from(u in User,
where: u.id != ^user.id,
where: u.local == true,
order_by: fragment("RANDOM()"),
limit: 10
)
)
end)
IO.puts("Fetching local users take #{to_sec(time)} sec.\n")
IO.puts("Fetching remote users...")
{time, remote_users} =
:timer.tc(fn ->
Repo.all(
from(u in User,
where: u.id != ^user.id,
where: u.local == false,
order_by: fragment("RANDOM()"),
limit: 10
)
)
end)
IO.puts("Fetching remote users take #{to_sec(time)} sec.\n")
generate_activities(user, users)
generate_remote_activities(user, remote_users)
generate_like_activities(
user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create"))
)
generate_dms(user, users, opts)
{:ok, activity} = generate_long_thread(user, users, opts)
generate_non_visible_message(user, users)
IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}") IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}")
@ -120,19 +62,6 @@ def run(args) do
"Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}" "Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}"
) )
fetch_user(user) Pleroma.LoadTesting.Fetcher.run_benchmarks(user)
query_timelines(user)
query_notifications(user)
query_dms(user)
query_long_thread(user, activity)
Pleroma.Config.put([:instance, :skip_thread_containment], false)
query_timelines(user)
end
defp clean_tables do
IO.puts("Deleting old data...\n")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
end end
end end

View File

@ -39,7 +39,7 @@
adapter: Ecto.Adapters.Postgres, adapter: Ecto.Adapters.Postgres,
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
database: "pleroma_test", database: "pleroma_benchmark",
hostname: System.get_env("DB_HOST") || "localhost", hostname: System.get_env("DB_HOST") || "localhost",
pool_size: 10 pool_size: 10
@ -61,8 +61,6 @@
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma_job_queue, disabled: true
config :pleroma, Pleroma.ScheduledActivity, config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2, daily_user_limit: 2,
total_user_limit: 3, total_user_limit: 3,

View File

@ -58,20 +58,6 @@
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
# Upload configuration # Upload configuration
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
@ -85,7 +71,9 @@
follow_redirect: true, follow_redirect: true,
pool: :upload pool: :upload
] ]
] ],
filename_display_max_length: 30,
default_description: nil
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
@ -110,6 +98,7 @@
"dat", "dat",
"dweb", "dweb",
"gopher", "gopher",
"hyper",
"ipfs", "ipfs",
"ipns", "ipns",
"irc", "irc",
@ -190,14 +179,18 @@
config :pleroma, :http, config :pleroma, :http,
proxy_url: nil, proxy_url: nil,
send_user_agent: true, send_user_agent: true,
user_agent: :default user_agent: :default,
adapter: []
config :pleroma, :instance, config :pleroma, :instance,
name: "Pleroma", name: "Pleroma",
email: "example@example.com", email: "example@example.com",
notify_email: "noreply@example.com", notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server", description: "Pleroma: An efficient and flexible fediverse server",
background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000, limit: 5_000,
description_limit: 5_000,
chat_limit: 5_000, chat_limit: 5_000,
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
@ -213,6 +206,7 @@
registrations_open: true, registrations_open: true,
invites_enabled: false, invites_enabled: false,
account_activation_required: false, account_activation_required: false,
account_approval_required: false,
federating: true, federating: true,
federation_incoming_replies_max_depth: 100, federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7, federation_reachability_timeout_days: 7,
@ -220,7 +214,6 @@
Pleroma.Web.ActivityPub.Publisher Pleroma.Web.ActivityPub.Publisher
], ],
allow_relay: true, allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true, public: true,
quarantined_instances: [], quarantined_instances: [],
managed_config: true, managed_config: true,
@ -231,13 +224,9 @@
"text/markdown", "text/markdown",
"text/bbcode" "text/bbcode"
], ],
mrf_transparency: true,
mrf_transparency_exclusions: [],
autofollowed_nicknames: [], autofollowed_nicknames: [],
max_pinned_statuses: 1, max_pinned_statuses: 1,
attachment_links: false, attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000, max_report_comment_size: 1000,
safe_dm_mentions: false, safe_dm_mentions: false,
healthcheck: false, healthcheck: false,
@ -250,9 +239,41 @@
max_remote_account_fields: 20, max_remote_account_fields: 20,
account_field_name_length: 512, account_field_name_length: 512,
account_field_value_length: 2048, account_field_value_length: 2048,
registration_reason_length: 500,
external_user_synchronization: true, external_user_synchronization: true,
extended_nickname_format: true, extended_nickname_format: true,
cleanup_attachments: false cleanup_attachments: false,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 5,
length: 16
]
],
show_reactions: true
config :pleroma, :welcome,
direct_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
chat_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
email: [
enabled: false,
sender: nil,
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
config :pleroma, :feed, config :pleroma, :feed,
post_title: %{ post_title: %{
@ -274,20 +295,33 @@
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
pleroma_fe: %{ pleroma_fe: %{
theme: "pleroma-dark", alwaysShowSubjectInput: true,
logo: "/static/logo.png",
background: "/images/city.jpg", background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false, collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false, hidePostStats: false,
hideSitename: false,
hideUserStats: false, hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.png",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: true, scopeCopy: true,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
subjectLineBehavior: "email", subjectLineBehavior: "email",
alwaysShowSubjectInput: true theme: "pleroma-dark",
webPushNotifications: false
}, },
masto_fe: %{ masto_fe: %{
showInstanceSpecificPanel: true showInstanceSpecificPanel: true
@ -346,9 +380,11 @@
federated_timeline_removal: [], federated_timeline_removal: [],
report_removal: [], report_removal: [],
reject: [], reject: [],
followers_only: [],
accept: [], accept: [],
avatar_removal: [], avatar_removal: [],
banner_removal: [] banner_removal: [],
reject_deletes: []
config :pleroma, :mrf_keyword, config :pleroma, :mrf_keyword,
reject: [], reject: [],
@ -357,12 +393,15 @@
config :pleroma, :mrf_subchain, match_actor: %{} config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365
config :pleroma, :mrf_vocabulary, config :pleroma, :mrf_vocabulary,
accept: [], accept: [],
reject: [] reject: []
# threshold of 7 days
config :pleroma, :mrf_object_age, config :pleroma, :mrf_object_age,
threshold: 172_800, threshold: 604_800,
actions: [:delist, :strip_followers] actions: [:delist, :strip_followers]
config :pleroma, :rich_media, config :pleroma, :rich_media,
@ -371,13 +410,16 @@
ignore_tld: ["local", "localdomain", "lan"], ignore_tld: ["local", "localdomain", "lan"],
parsers: [ parsers: [
Pleroma.Web.RichMedia.Parsers.TwitterCard, Pleroma.Web.RichMedia.Parsers.TwitterCard,
Pleroma.Web.RichMedia.Parsers.OGP,
Pleroma.Web.RichMedia.Parsers.OEmbed Pleroma.Web.RichMedia.Parsers.OEmbed
], ],
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl] ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,
invalidation: [
enabled: false,
provider: Pleroma.Web.MediaProxy.Invalidation.Script
],
proxy_opts: [ proxy_opts: [
redirect_on_failure: false, redirect_on_failure: false,
max_body_length: 25 * 1_048_576, max_body_length: 25 * 1_048_576,
@ -388,6 +430,13 @@
], ],
whitelist: [] whitelist: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge,
headers: [],
options: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
config :pleroma, :chat, enabled: true config :pleroma, :chat, enabled: true
config :phoenix, :format_encoders, json: Jason config :phoenix, :format_encoders, json: Jason
@ -410,6 +459,11 @@
], ],
unfurl_nsfw: false unfurl_nsfw: false
config :pleroma, Pleroma.Web.Preload,
providers: [
Pleroma.Web.Preload.Providers.Instance
]
config :pleroma, :http_security, config :pleroma, :http_security,
enabled: true, enabled: true,
sts: false, sts: false,
@ -462,13 +516,19 @@
"user-search", "user-search",
"user_exists", "user_exists",
"users", "users",
"web" "web",
] "verify_credentials",
"update_credentials",
"relationships",
"search",
"confirmation_resend",
"mfa"
],
email_blacklist: []
config :pleroma, Oban, config :pleroma, Oban,
repo: Pleroma.Repo, repo: Pleroma.Repo,
verbose: false, log: false,
prune: {:maxlen, 1500},
queues: [ queues: [
activity_expiration: 10, activity_expiration: 10,
federator_incoming: 50, federator_incoming: 50,
@ -482,6 +542,7 @@
attachments_cleanup: 5, attachments_cleanup: 5,
new_users_digest: 1 new_users_digest: 1
], ],
plugins: [Oban.Plugins.Pruner],
crontab: [ crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker}, {"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
@ -496,16 +557,14 @@
federator_outgoing: 5 federator_outgoing: 5
] ]
config :auto_linker, config :pleroma, Pleroma.Formatter,
opts: [
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false, class: false,
strip_prefix: false, rel: "ugc",
new_window: false, new_window: false,
rel: "ugc" truncate: false,
] strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :pleroma, :ldap, config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true", enabled: System.get_env("LDAP_ENABLED") == "true",
@ -604,6 +663,59 @@
config :pleroma, :static_fe, enabled: false config :pleroma, :static_fe, enabled: false
# Example of frontend configuration
# This example will make us serve the primary frontend from the
# frontends directory within your `:pleroma, :instance, static_dir`.
# e.g., instance/static/frontends/pleroma/develop/
#
# With no frontend configuration, the bundled files from the `static` directory will
# be used.
#
# config :pleroma, :frontends,
# primary: %{"name" => "pleroma-fe", "ref" => "develop"},
# admin: %{"name" => "admin-fe", "ref" => "stable"},
# available: %{...}
config :pleroma, :frontends,
available: %{
"kenoma" => %{
"name" => "kenoma",
"git" => "https://git.pleroma.social/lambadalambda/kenoma",
"build_url" =>
"https://git.pleroma.social/lambadalambda/kenoma/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "master"
},
"pleroma-fe" => %{
"name" => "pleroma-fe",
"git" => "https://git.pleroma.social/pleroma/pleroma-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "develop"
},
"fedi-fe" => %{
"name" => "fedi-fe",
"git" => "https://git.pleroma.social/pleroma/fedi-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "master"
},
"admin-fe" => %{
"name" => "admin-fe",
"git" => "https://git.pleroma.social/pleroma/admin-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "develop"
},
"soapbox-fe" => %{
"name" => "soapbox-fe",
"git" => "https://gitlab.com/soapbox-pub/soapbox-fe",
"build_url" =>
"https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production",
"ref" => "v1.0.0",
"build_dir" => "static"
}
}
config :pleroma, :web_cache_ttl, config :pleroma, :web_cache_ttl,
activity_pub: nil, activity_pub: nil,
activity_pub_question: 30_000 activity_pub_question: 30_000
@ -616,6 +728,71 @@
parameters: [gin_fuzzy_search_limit: "500"], parameters: [gin_fuzzy_search_limit: "500"],
prepare: :unnamed prepare: :unnamed
config :pleroma, :connections_pool,
reclaim_multiplier: 0.1,
connection_acquisition_wait: 250,
connection_acquisition_retries: 5,
max_connections: 250,
max_idle_time: 30_000,
retry: 0,
await_up_timeout: 5_000
config :pleroma, :pools,
federation: [
size: 50,
max_waiting: 10
],
media: [
size: 50,
max_waiting: 10
],
upload: [
size: 25,
max_waiting: 5
],
default: [
size: 10,
max_waiting: 2
]
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
private_instance? = :if_instance_is_private
config :pleroma, :restrict_unauthenticated,
timelines: %{local: private_instance?, federated: private_instance?},
profiles: %{local: private_instance?, remote: private_instance?},
activities: %{local: private_instance?, remote: private_instance?}
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
config :pleroma, :mrf,
policies: Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy,
transparency: true,
transparency_exclusions: []
config :tzdata, :http_client, Pleroma.HTTP.Tzdata
config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :pleroma, :instances_favicons, enabled: false
config :floki, :html_parser, Floki.HTMLParser.FastHtml
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,8 @@
hostname: "localhost", hostname: "localhost",
pool_size: 10 pool_size: 10
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
if File.exists?("./config/dev.secret.exs") do if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs" import_config "dev.secret.exs"
else else

View File

@ -21,7 +21,10 @@
config :pleroma, :auth, oauth_consumer_strategies: [] config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload, filters: [], link_name: false config :pleroma, Pleroma.Upload,
filters: [],
link_name: false,
default_description: :filename
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
@ -56,6 +59,19 @@
ignore_hosts: [], ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"] ignore_tld: ["local", "localdomain", "lan"]
config :pleroma, :instance,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
config :web_push_encryption, :vapid_details, config :web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com", subject: "mailto:administrator@example.com",
public_key: public_key:
@ -66,8 +82,8 @@
config :pleroma, Oban, config :pleroma, Oban,
queues: false, queues: false,
prune: :disabled, crontab: false,
crontab: false plugins: false
config :pleroma, Pleroma.ScheduledActivity, config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2, daily_user_limit: 2,
@ -90,10 +106,25 @@
config :pleroma, :modules, runtime_dir: "test/fixtures/modules" config :pleroma, :modules, runtime_dir: "test/fixtures/modules"
config :pleroma, Pleroma.Gun, Pleroma.GunMock
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
config :pleroma, :instances_favicons, enabled: true
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: nil
config :tzdata, :autoupdate, :disabled
config :pleroma, :mrf, policies: []
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

6
coveralls.json Normal file
View File

@ -0,0 +1,6 @@
{
"skip_files": [
"test/support",
"lib/mix/tasks/pleroma/benchmark.ex"
]
}

View File

@ -19,6 +19,7 @@ Configuration options:
- `local`: only local users - `local`: only local users
- `external`: only external users - `external`: only external users
- `active`: only active users - `active`: only active users
- `need_approval`: only unapproved users
- `deactivated`: only deactivated users - `deactivated`: only deactivated users
- `is_admin`: users with admin role - `is_admin`: users with admin role
- `is_moderator`: users with moderator role - `is_moderator`: users with moderator role
@ -46,7 +47,10 @@ Configuration options:
"local": bool, "local": bool,
"tags": array, "tags": array,
"avatar": string, "avatar": string,
"display_name": string "display_name": string,
"confirmation_pending": bool,
"approval_pending": bool,
"registration_reason": string,
}, },
... ...
] ]
@ -242,6 +246,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `PATCH /api/pleroma/admin/users/approve`
### Approve user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `GET /api/pleroma/admin/users/:nickname_or_id` ## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user ### Retrive the details of a user
@ -291,31 +313,53 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On failure: `Not found` - On failure: `Not found`
- On success: JSON array of user's latest statuses - On success: JSON array of user's latest statuses
## `GET /api/pleroma/admin/relay`
### List Relays
Params: none
Response:
* On success: JSON array of relays
```json
[
{"actor": "https://example.com/relay", "followed_back": true},
{"actor": "https://example2.com/relay", "followed_back": false}
]
```
## `POST /api/pleroma/admin/relay` ## `POST /api/pleroma/admin/relay`
### Follow a Relay ### Follow a Relay
- Params: Params:
- `relay_url`
- Response: * `relay_url`
- On success: URL of the followed relay
Response:
* On success: relay json object
```json
{"actor": "https://example.com/relay", "followed_back": true}
```
## `DELETE /api/pleroma/admin/relay` ## `DELETE /api/pleroma/admin/relay`
### Unfollow a Relay ### Unfollow a Relay
- Params: Params:
- `relay_url`
- Response:
- On success: URL of the unfollowed relay
## `GET /api/pleroma/admin/relay` * `relay_url`
### List Relays Response:
- Params: none * On success: URL of the unfollowed relay
- Response:
- On success: JSON array of relays ```json
{"https://example.com/relay"}
```
## `POST /api/pleroma/admin/users/invite_token` ## `POST /api/pleroma/admin/users/invite_token`
@ -392,10 +436,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `email` - `email`
- `name`, optional - `name`, optional
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request, JSON:
```json
[
{
"error": "Appropriate error message here"
}
]
```
## `GET /api/pleroma/admin/users/:nickname/password_reset` ## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname
- Params: none - Params: none
- Response: - Response:
@ -414,6 +472,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nicknames` - `nicknames`
- Response: none (code `204`) - Response: none (code `204`)
## PUT `/api/pleroma/admin/users/disable_mfa`
### Disable mfa for user's account.
- Params:
- `nickname`
- Response: Users nickname
## `GET /api/pleroma/admin/users/:nickname/credentials` ## `GET /api/pleroma/admin/users/:nickname/credentials`
### Get the user's email, password, display and settings-related fields ### Get the user's email, password, display and settings-related fields
@ -466,30 +532,52 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
### Change the user's email, password, display and settings-related fields ### Change the user's email, password, display and settings-related fields
- Params: * Params:
- `email` * `email`
- `password` * `password`
- `name` * `name`
- `bio` * `bio`
- `avatar` * `avatar`
- `locked` * `locked`
- `no_rich_text` * `no_rich_text`
- `default_scope` * `default_scope`
- `banner` * `banner`
- `hide_follows` * `hide_follows`
- `hide_followers` * `hide_followers`
- `hide_followers_count` * `hide_followers_count`
- `hide_follows_count` * `hide_follows_count`
- `hide_favorites` * `hide_favorites`
- `allow_following_move` * `allow_following_move`
- `background` * `background`
- `show_role` * `show_role`
- `skip_thread_containment` * `skip_thread_containment`
- `fields` * `fields`
- `discoverable` * `discoverable`
- `actor_type` * `actor_type`
- Response: none (code `200`) * Responses:
Status: 200
```json
{"status": "success"}
```
Status: 400
```json
{"errors":
{"actor_type": "is invalid"},
{"email": "has invalid format"},
...
}
```
Status: 404
```json
{"error": "Not found"}
```
## `GET /api/pleroma/admin/reports` ## `GET /api/pleroma/admin/reports`
@ -509,7 +597,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```json ```json
{ {
"totalReports" : 1, "total" : 1,
"reports": [ "reports": [
{ {
"account": { "account": {
@ -730,7 +818,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing - 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response - On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:report_id/notes/:id` ## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id`
### Delete report note ### Delete report note
@ -742,6 +830,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing - 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response - On success: `204`, empty response
## `GET /api/pleroma/admin/statuses/:id`
### Show status by id
- Params:
- `id`: required, status id
- Response:
- On failure:
- 404 Not Found `"Not Found"`
- On success: JSON, Mastodon Status entity
## `PUT /api/pleroma/admin/statuses/:id` ## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status ### Change the scope of an individual reported status
@ -855,6 +954,8 @@ Some modifications are necessary to save the config settings correctly:
Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as: Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
- all settings inside these keys: - all settings inside these keys:
- `:hackney_pools` - `:hackney_pools`
- `:connections_pool`
- `:pools`
- `:chat` - `:chat`
- partially settings inside these keys: - partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha` - `:seconds_valid` in `Pleroma.Captcha`
@ -1061,6 +1162,10 @@ Loads json generated from `config/descriptions.exs`.
### Stats ### Stats
- Query Params:
- *optional* `instance`: **string** instance hostname (without protocol) to get stats for
- Example: `https://mypleroma.org/api/pleroma/admin/stats?instance=lain.com`
- Response: - Response:
```json ```json
@ -1073,3 +1178,159 @@ Loads json generated from `config/descriptions.exs`.
} }
} }
``` ```
## `GET /api/pleroma/admin/oauth_app`
### List OAuth app
- Params:
- *optional* `name`
- *optional* `client_id`
- *optional* `page`
- *optional* `page_size`
- *optional* `trusted`
- Response:
```json
{
"apps": [
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
],
"count": 17,
"page_size": 50
}
```
## `POST /api/pleroma/admin/oauth_app`
### Create OAuth App
- Params:
- `name`
- `redirect_uris`
- `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
- On failure:
```json
{
"redirect_uris": "can't be blank",
"name": "can't be blank"
}
```
## `PATCH /api/pleroma/admin/oauth_app/:id`
### Update OAuth App
- Params:
- *optional* `name`
- *optional* `redirect_uris`
- *optional* `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
## `DELETE /api/pleroma/admin/oauth_app/:id`
### Delete OAuth App
- Params: None
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
## `GET /api/pleroma/admin/media_proxy_caches`
### Get a list of all banned MediaProxy URLs in Cachex
- Authentication: required
- Params:
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `query`: **string** search term
- Response:
``` json
{
"page_size": integer,
"count": integer,
"urls": [
"http://example.com/media/a688346.jpg",
"http://example.com/media/fb1f4d.jpg"
]
}
```
## `POST /api/pleroma/admin/media_proxy_caches/delete`
### Remove a banned MediaProxy URL from Cachex
- Authentication: required
- Params:
- `urls` (array)
- Response:
``` json
{ }
```
## `POST /api/pleroma/admin/media_proxy_caches/purge`
### Purge a MediaProxy URL
- Authentication: required
- Params:
- `urls` (array)
- `ban` (boolean)
- Response:
``` json
{ }
```

248
docs/API/chats.md Normal file
View File

@ -0,0 +1,248 @@
# Chats
Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common.
## Why Chats?
There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API.
This is an awkward setup for a few reasons:
- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much")
- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation.
- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message.
- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public.
As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly.
## Chats explained
For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview:
- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future.
- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them.
- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field.
- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued.
- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person.
- `ChatMessage`s don't show up in the existing timelines.
- Chats can never go from private to public. They are always private between the two actors.
## Caveats
- Chats are NOT E2E encrypted (yet). Security is still the same as email.
## API
In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`.
This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`.
### Creating or getting a chat.
To create or get an existing Chat for a certain recipient (identified by Account ID)
you can call:
`POST /api/v1/pleroma/chats/by-account-id/:account_id`
The account id is the normal FlakeId of the user
```
POST /api/v1/pleroma/chats/by-account-id/someflakeid
```
If you already have the id of a chat, you can also use
```
GET /api/v1/pleroma/chats/:id
```
There will only ever be ONE Chat for you and a given recipient, so this call
will return the same Chat if you already have one with that user.
Returned data:
```json
{
"account": {
"id": "someflakeid",
"username": "somenick",
...
},
"id" : "1",
"unread" : 2,
"last_message" : {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
### Marking a chat as read
To mark a number of messages in a chat up to a certain message as read, you can use
`POST /api/v1/pleroma/chats/:id/read`
Parameters:
- last_read_id: Given this id, all chat messages until this one will be marked as read. Required.
Returned data:
```json
{
"account": {
"id": "someflakeid",
"username": "somenick",
...
},
"id" : "1",
"unread" : 0,
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
### Marking a single chat message as read
To set the `unread` property of a message to `false`
`POST /api/v1/pleroma/chats/:id/messages/:message_id/read`
Returned data:
The modified chat message
### Getting a list of Chats
`GET /api/v1/pleroma/chats`
This will return a list of chats that you have been involved in, sorted by their
last update (so new chats will be at the top).
Returned data:
```json
[
{
"account": {
"id": "someflakeid",
"username": "somenick",
...
},
"id" : "1",
"unread" : 2,
"last_message" : {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
]
```
The recipient of messages that are sent to this chat is given by their AP ID.
No pagination is implemented for now.
### Getting the messages for a Chat
For a given Chat id, you can get the associated messages with
`GET /api/v1/pleroma/chats/:id/messages`
This will return all messages, sorted by most recent to least recent. The usual
pagination options are implemented.
Returned data:
```json
[
{
"account_id": "someflakeid",
"chat_id": "1",
"content": "Check this out :firefox:",
"created_at": "2020-04-21T15:11:46.000Z",
"emojis": [
{
"shortcode": "firefox",
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
"url": "https://dontbulling.me/emoji/Firefox.gif",
"visible_in_picker": false
}
],
"id": "13",
"unread": true
},
{
"account_id": "someflakeid",
"chat_id": "1",
"content": "Whats' up?",
"created_at": "2020-04-21T15:06:45.000Z",
"emojis": [],
"id": "12",
"unread": false
}
]
```
### Posting a chat message
Posting a chat message for given Chat id works like this:
`POST /api/v1/pleroma/chats/:id/messages`
Parameters:
- content: The text content of the message. Optional if media is attached.
- media_id: The id of an upload that will be attached to the message.
Currently, no formatting beyond basic escaping and emoji is implemented.
Returned data:
```json
{
"account_id": "someflakeid",
"chat_id": "1",
"content": "Check this out :firefox:",
"created_at": "2020-04-21T15:11:46.000Z",
"emojis": [
{
"shortcode": "firefox",
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
"url": "https://dontbulling.me/emoji/Firefox.gif",
"visible_in_picker": false
}
],
"id": "13",
"unread": false
}
```
### Deleting a chat message
Deleting a chat message for given Chat id works like this:
`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id`
Returned data is the deleted message.
### Notifications
There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:
```json
{
"id": "someid",
"type": "pleroma:chat_mention",
"account": { ... } // User account of the sender,
"chat_message": {
"chat_id": "1",
"id": "10",
"content": "Hello",
"account_id": "someflakeid",
"unread": false
},
"created_at": "somedate"
}
```
### Streaming
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
### Web Push
If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription.

View File

@ -4,16 +4,13 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
## Flake IDs ## Flake IDs
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are sortable strings Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
## Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
## Timelines ## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`. Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
## Statuses ## Statuses
@ -30,13 +27,22 @@ Has these additional fields under the `pleroma` object:
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted - `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not.
## Attachments ## Media Attachments
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment. - `mime_type`: mime type of the attachment.
### Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
### Limitations
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
## Accounts ## Accounts
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc. The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
@ -46,20 +52,27 @@ The `id` parameter can also be the `nickname` of the user. This only works in th
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
- `ap_id`: nullable URL string, ActivityPub id of the user
- `background_image`: nullable URL string, background image of the user
- `tags`: Lists an array of tags for the user - `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/ - `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
- `is_moderator`: boolean, nullable, true if user is a moderator - `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin - `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 - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_favorites`: boolean, true when the user has hiding favorites enabled
- `hide_followers`: boolean, true when the user has follower hiding enabled - `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled - `hide_follows`: boolean, true when the user has follow hiding enabled
- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled - `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled - `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `/api/v1/accounts/verify_credentials`
- `deactivated`: boolean, true when the user is deactivated - `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
- `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
### Source ### Source
@ -117,7 +130,19 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
Accepts additional parameters: Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
- `with_move`: boolean, when set to `true` will include Move notifications. `false` by default. - `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
## DELETE `/api/v1/notifications/destroy_multiple`
An endpoint to delete multiple statuses by IDs.
Required parameters:
- `ids`: array of activity ids
Usage example: `DELETE /api/v1/notifications/destroy_multiple/?ids[]=1&ids[]=2`.
Returns on success: 200 OK `{}`
## POST `/api/v1/statuses` ## POST `/api/v1/statuses`
@ -144,7 +169,7 @@ Returns: array of Status.
The maximum number of statuses is limited to 100 per request. The maximum number of statuses is limited to 100 per request.
## PATCH `/api/v1/update_credentials` ## PATCH `/api/v1/accounts/update_credentials`
Additional parameters can be added to the JSON body/Form data: Additional parameters can be added to the JSON body/Form data:
@ -159,30 +184,107 @@ Additional parameters can be added to the JSON body/Form data:
- `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `allow_following_move` - if true, allows automatically follow moved following accounts
- `pleroma_background_image` - sets the background image of the user. - `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, discovery of this account in search results and other services is allowed. - `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account. - `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
### Pleroma Settings Store ### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings. The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
This information is returned in the `verify_credentials` endpoint. This information is returned in the `/api/v1/accounts/verify_credentials` endpoint.
## Authentication ## Authentication
*Pleroma supports refreshing tokens. *Pleroma supports refreshing tokens.*
`POST /oauth/token` `POST /oauth/token`
Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
Post here request with `grant_type=refresh_token` to obtain new access token. Returns an access token.
## Account Registration ## Account Registration
`POST /api/v1/accounts` `POST /api/v1/accounts`
Has theses additionnal parameters (which are the same as in Pleroma-API): Has theses additional parameters (which are the same as in Pleroma-API):
* `fullname`: optional
* `bio`: optional - `fullname`: optional
* `captcha_solution`: optional, contains provider-specific captcha solution, - `bio`: optional
* `captcha_token`: optional, contains provider-specific captcha token - `captcha_solution`: optional, contains provider-specific captcha solution,
* `token`: invite token required when the registerations aren't public. - `captcha_token`: optional, contains provider-specific captcha token
- `captcha_answer_data`: optional, contains provider-specific captcha data
- `token`: invite token required when the registrations aren't public.
## Instance
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
- `chat_limit`: The maximum characters per chat message
- `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars
- `background_upload_limit`: The same for backgrounds
- `banner_upload_limit`: The same for banners
- `background_image`: A background image that frontends can use
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
- `pleroma.metadata.post_formats`: A list of the allowed post format types
- `vapid_public_key`: The public key needed for push messages
## Markers
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications
## Streaming
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
## Not implemented
Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority.
### Suggestions
*Added in Mastodon 2.4.3*
- `GET /api/v1/suggestions`: Returns an empty array, `[]`
### Trends
*Added in Mastodon 3.0.0*
- `GET /api/v1/trends`: Returns an empty array, `[]`
### Identity proofs
*Added in Mastodon 2.8.0*
- `GET /api/v1/identity_proofs`: Returns an empty array, `[]`
### Endorsements
*Added in Mastodon 2.5.0*
- `GET /api/v1/endorsements`: Returns an empty array, `[]`
### Profile directory
*Added in Mastodon 3.0.0*
- `GET /api/v1/directory`: Returns HTTP 404
### Featured tags
*Added in Mastodon 3.0.0*
- `GET /api/v1/featured_tags`: Returns HTTP 404

View File

@ -50,7 +50,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Authentication: not required * Authentication: not required
* Params: none * 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"}` * Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint", "seconds_valid": 300}`
## `/api/pleroma/delete_account` ## `/api/pleroma/delete_account`
### Delete an account ### Delete an account
@ -70,7 +70,49 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise * Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}` * Example response: `{"error": "Invalid password."}`
## `/api/pleroma/admin/` ## `/api/pleroma/accounts/mfa`
#### Gets current MFA settings
* method: `GET`
* Authentication: required
* OAuth scope: `read:security`
* Response: JSON. Returns `{"enabled": "false", "totp": false }`
## `/api/pleroma/accounts/mfa/setup/totp`
#### Pre-setup the MFA/TOTP method
* method: `GET`
* Authentication: required
* OAuth scope: `write:security`
* Response: JSON. Returns `{"key": [secret_key], "provisioning_uri": "[qr code uri]" }` when successful, otherwise returns HTTP 422 `{"error": "error_msg"}`
## `/api/pleroma/accounts/mfa/confirm/totp`
#### Confirms & enables MFA/TOTP support for user account.
* method: `POST`
* Authentication: required
* OAuth scope: `write:security`
* Params:
* `password`: user's password
* `code`: token from TOTP App
* Response: JSON. Returns `{}` if the enable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
## `/api/pleroma/accounts/mfa/totp`
#### Disables MFA/TOTP method for user account.
* method: `DELETE`
* Authentication: required
* OAuth scope: `write:security`
* Params:
* `password`: user's password
* Response: JSON. Returns `{}` if the disable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/pleroma/accounts/mfa/backup_codes`
#### Generstes backup codes MFA for user account.
* method: `GET`
* Authentication: required
* OAuth scope: `write:security`
* Response: JSON. Returns `{"codes": codes}`when successful, otherwise HTTP 422 `{"error": "[error message]"}`
## `/api/pleroma/admin/`
See [Admin-API](admin_api.md) See [Admin-API](admin_api.md)
## `/api/v1/pleroma/notifications/read` ## `/api/v1/pleroma/notifications/read`
@ -223,7 +265,7 @@ See [Admin-API](admin_api.md)
* Method `PUT` * Method `PUT`
* Authentication: required * Authentication: required
* Params: * Params:
* `image`: Multipart image * `file`: Multipart image
* Response: JSON. Returns a mastodon media attachment entity * Response: JSON. Returns a mastodon media attachment entity
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}` when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
* Example response: * Example response:
@ -245,11 +287,8 @@ See [Admin-API](admin_api.md)
* Method `PUT` * Method `PUT`
* Authentication: required * Authentication: required
* Params: * Params:
* `followers`: BOOLEAN field, receives notifications from followers * `block_from_strangers`: BOOLEAN field, blocks notifications from accounts you do not follow
* `follows`: BOOLEAN field, receives notifications from people the user follows * `hide_notification_contents`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
* `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance
* `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
## `/api/pleroma/healthcheck` ## `/api/pleroma/healthcheck`
@ -316,27 +355,61 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy) * Response: JSON, statuses (200 - healthy, 503 unhealthy)
## `GET /api/v1/pleroma/conversations/read` ## `POST /api/v1/pleroma/conversations/read`
### Marks all user's conversations as read. ### Marks all user's conversations as read.
* Method `POST` * Method `POST`
* Authentication: required * Authentication: required
* Params: None * Params: None
* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy). * Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy).
## `GET /api/pleroma/emoji/packs` ## `GET /api/pleroma/emoji/packs/import`
### Lists the custom emoji packs on the server ### Imports packs from filesystem
* Method `GET` * Method `GET`
* Authentication: not required * Authentication: required
* Params: None * Params: None
* Response: JSON, "ok" and 200 status and the JSON hashmap of "pack name" to "pack contents" * Response: JSON, returns a list of imported packs.
## `PUT /api/pleroma/emoji/packs/:name` ## `GET /api/pleroma/emoji/packs/remote`
### Creates an empty custom emoji pack ### Make request to another instance for packs list
* Method `PUT` * Method `GET`
* Authentication: required
* Params:
* `url`: url of the instance to get packs from
* Response: JSON with the pack list, hashmap with pack name and pack contents
## `POST /api/pleroma/emoji/packs/download`
### Download pack from another instance
* Method `POST`
* Authentication: required
* Params:
* `url`: url of the instance to download from
* `name`: pack to download from that instance
* `as`: (*optional*) name how to save pack
* Response: JSON, "ok" with 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack
## `POST /api/pleroma/emoji/packs/:name`
### Creates an empty pack
* Method `POST`
* Authentication: required * Authentication: required
* Params: None * Params: None
* Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists * Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists
## `PATCH /api/pleroma/emoji/packs/:name`
### Updates (replaces) pack metadata
* Method `PATCH`
* Authentication: required
* Params:
* `metadata`: metadata to replace the old one
* `license`: Pack license
* `homepage`: Pack home page url
* `description`: Pack description
* `fallback-src`: Fallback url to download pack from
* `fallback-src-sha256`: SHA256 encoded for fallback pack archive
* `share-files`: is pack allowed for sharing (boolean)
* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a
problem with the new metadata (the error is specified in the "error" part of the response JSON)
## `DELETE /api/pleroma/emoji/packs/:name` ## `DELETE /api/pleroma/emoji/packs/:name`
### Delete a custom emoji pack ### Delete a custom emoji pack
* Method `DELETE` * Method `DELETE`
@ -344,53 +417,77 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Params: None * Params: None
* Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack * Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack
## `POST /api/pleroma/emoji/packs/:name/update_file` ## `POST /api/pleroma/emoji/packs/:name/files`
### Update a file in a custom emoji pack ### Add new file to the pack
* Method `POST` * Method `POST`
* Authentication: required * Authentication: required
* Params: * Params:
* if the `action` is `add`, adds an emoji named `shortcode` to the pack `pack_name`, * `file`: file needs to be uploaded with the multipart request or link to remote file.
that means that the emoji file needs to be uploaded with the request * `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename.
(thus requiring it to be a multipart request) and be named `file`. * `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename.
There can also be an optional `filename` that will be the new emoji file name * Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
(if it's not there, the name will be taken from the uploaded file).
* if the `action` is `update`, changes emoji shortcode
(from `shortcode` to `new_shortcode` or moves the file (from the current filename to `new_filename`)
* if the `action` is `remove`, removes the emoji named `shortcode` and it's associated file
* Response: JSON, updated "files" section of the pack and 200 status, 409 if the trying to use a shortcode
that is already taken, 400 if there was an error with the shortcode, filename or file (additional info
in the "error" part of the response JSON)
## `POST /api/pleroma/emoji/packs/:name/update_metadata` ## `PATCH /api/pleroma/emoji/packs/:name/files`
### Updates (replaces) pack metadata ### Update emoji file from pack
* Method `POST` * Method `PATCH`
* Authentication: required * Authentication: required
* Params: * Params:
* `new_data`: new metadata to replace the old one * `shortcode`: emoji file shortcode
* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a * `new_shortcode`: new emoji file shortcode
problem with the new metadata (the error is specified in the "error" part of the response JSON) * `new_filename`: new filename for emoji file
* `force`: (*optional*) with true value to overwrite existing emoji with new shortcode
* Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
## `POST /api/pleroma/emoji/packs/download_from` ## `DELETE /api/pleroma/emoji/packs/:name/files`
### Requests the instance to download the pack from another instance ### Delete emoji file from pack
* Method `POST` * Method `DELETE`
* Authentication: required * Authentication: required
* Params: * Params:
* `instance_address`: the address of the instance to download from * `shortcode`: emoji file shortcode
* `pack_name`: the pack to download from that instance * Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack
## `POST /api/pleroma/emoji/packs/list_from` ## `GET /api/pleroma/emoji/packs`
### Requests the instance to list the packs from another instance
* Method `POST` ### Lists local custom emoji packs
* Authentication: required
* Method `GET`
* Authentication: not required
* Params: * Params:
* `instance_address`: the address of the instance to download from * `page`: page number for packs (default 1)
* Response: JSON with the pack list, same as if the request was made to that instance's * `page_size`: page size for packs (default 50)
list endpoint directly + 200 status * Response: `packs` key with JSON hashmap of pack name to pack contents and `count` key for count of packs.
## `GET /api/pleroma/emoji/packs/:name/download_shared` ```json
### Requests a local pack from the instance {
"packs": {
"pack_name": {...}, // pack contents
...
},
"count": 0 // packs count
}
```
## `GET /api/pleroma/emoji/packs/:name`
### Get pack.json for the pack
* Method `GET`
* Authentication: not required
* Params:
* `page`: page number for files (default 1)
* `page_size`: page size for files (default 30)
* Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist.
```json
{
"files": {...},
"files_count": 0, // emoji count in pack
"pack": {...}
}
```
## `GET /api/pleroma/emoji/packs/:name/archive`
### Requests a local pack archive from the instance
* Method `GET` * Method `GET`
* Authentication: not required * Authentication: not required
* Params: None * Params: None
@ -431,7 +528,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
# Emoji Reactions # Emoji Reactions
Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character. Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character. To detect the presence of this feature, you can check `pleroma_emoji_reactions` entry in the features list of nodeinfo.
## `PUT /api/v1/pleroma/statuses/:id/reactions/:emoji` ## `PUT /api/v1/pleroma/statuses/:id/reactions/:emoji`
### React to a post with a unicode emoji ### React to a post with a unicode emoji
@ -462,7 +559,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
``` ```
## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji` ## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji` ### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
* Method: `GET` * Method: `GET`
* Authentication: optional * Authentication: optional
* Params: None * Params: None

View File

@ -11,15 +11,18 @@
config :pleroma, configurable_from_database: true config :pleroma, configurable_from_database: true
``` ```
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl config migrate_to_db ./bin/pleroma_ctl config migrate_to_db
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.config migrate_to_db mix pleroma.config migrate_to_db
``` ```
## Transfer config from DB to `config/env.exported_from_db.secret.exs` ## Transfer config from DB to `config/env.exported_from_db.secret.exs`
!!! note !!! note
@ -31,10 +34,12 @@ mix pleroma.config migrate_to_db
To delete transfered settings from database optional flag `-d` can be used. `<env>` is `prod` by default. To delete transfered settings from database optional flag `-d` can be used. `<env>` is `prod` by default.
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl config migrate_from_db [--env=<env>] [-d] ./bin/pleroma_ctl config migrate_from_db [--env=<env>] [-d]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.config migrate_from_db [--env=<env>] [-d] mix pleroma.config migrate_from_db [--env=<env>] [-d]
``` ```

View File

@ -9,14 +9,19 @@
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. 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. Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. 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.
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl database remove_embedded_objects [option ...] ./bin/pleroma_ctl database remove_embedded_objects [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.database remove_embedded_objects [option ...] mix pleroma.database remove_embedded_objects [option ...]
``` ```
### Options ### Options
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
@ -27,11 +32,15 @@ This will prune remote posts older than 90 days (configurable with [`config :ple
!!! danger !!! danger
The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free. The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free.
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl database prune_objects [option ...] ./bin/pleroma_ctl database prune_objects [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.database prune_objects [option ...] mix pleroma.database prune_objects [option ...]
``` ```
@ -42,30 +51,93 @@ mix pleroma.database prune_objects [option ...]
Can be safely re-run Can be safely re-run
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl database bump_all_conversations ./bin/pleroma_ctl database bump_all_conversations
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.database bump_all_conversations mix pleroma.database bump_all_conversations
``` ```
## Remove duplicated items from following and update followers count for all users ## Remove duplicated items from following and update followers count for all users
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl database update_users_following_followers_counts ./bin/pleroma_ctl database update_users_following_followers_counts
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.database update_users_following_followers_counts mix pleroma.database update_users_following_followers_counts
``` ```
## Fix the pre-existing "likes" collections for all objects ## Fix the pre-existing "likes" collections for all objects
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl database fix_likes_collections ./bin/pleroma_ctl database fix_likes_collections
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.database fix_likes_collections mix pleroma.database fix_likes_collections
``` ```
## Vacuum the database
### Analyze
Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.**
=== "OTP"
```sh
./bin/pleroma_ctl database vacuum analyze
```
=== "From Source"
```sh
mix pleroma.database vacuum analyze
```
### Full
Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller
and more compact files with an optimized layout. This process will take a long time and use additional disk space as
it builds the files side-by-side the existing database files. It can make your database faster and use less disk space,
but should only be run if necessary. **It is safe to cancel this.**
=== "OTP"
```sh
./bin/pleroma_ctl database vacuum full
```
=== "From Source"
```sh
mix pleroma.database vacuum full
```
## Add expiration to all local statuses
=== "OTP"
```sh
./bin/pleroma_ctl database ensure_expiration
```
=== "From Source"
```sh
mix pleroma.database ensure_expiration
```

View File

@ -4,22 +4,30 @@
## Send digest email since given date (user registration date by default) ignoring user activity status. ## Send digest email since given date (user registration date by default) ignoring user activity status.
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl digest test <nickname> [since_date] ./bin/pleroma_ctl digest test <nickname> [since_date]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.digest test <nickname> [since_date] mix pleroma.digest test <nickname> [since_date]
``` ```
Example: Example:
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl digest test donaldtheduck 2019-05-20 ./bin/pleroma_ctl digest test donaldtheduck 2019-05-20
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.digest test donaldtheduck 2019-05-20 mix pleroma.digest test donaldtheduck 2019-05-20
``` ```

View File

@ -4,21 +4,29 @@
## Send test email (instance email by default) ## Send test email (instance email by default)
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl email test [--to <destination email address>] ./bin/pleroma_ctl email test [--to <destination email address>]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.email test [--to <destination email address>] mix pleroma.email test [--to <destination email address>]
``` ```
Example: Example:
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl email test --to root@example.org ./bin/pleroma_ctl email test --to root@example.org
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.email test --to root@example.org mix pleroma.email test --to root@example.org
``` ```

View File

@ -4,11 +4,13 @@
## Lists emoji packs and metadata specified in the manifest ## Lists emoji packs and metadata specified in the manifest
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl emoji ls-packs [option ...] ./bin/pleroma_ctl emoji ls-packs [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.emoji ls-packs [option ...] mix pleroma.emoji ls-packs [option ...]
``` ```
@ -18,11 +20,13 @@ mix pleroma.emoji ls-packs [option ...]
## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME` ## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME`
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl emoji get-packs [option ...] <pack ...> ./bin/pleroma_ctl emoji get-packs [option ...] <pack ...>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.emoji get-packs [option ...] <pack ...> mix pleroma.emoji get-packs [option ...] <pack ...>
``` ```
@ -31,16 +35,27 @@ mix pleroma.emoji get-packs [option ...] <pack ...>
## Create a new manifest entry and a file list from the specified remote pack file ## Create a new manifest entry and a file list from the specified remote pack file
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl emoji gen-pack PACK-URL ./bin/pleroma_ctl emoji gen-pack PACK-URL
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.emoji gen-pack PACK-URL mix pleroma.emoji gen-pack PACK-URL
``` ```
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. 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 manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) 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). 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).
## Reload emoji packs
=== "OTP"
```sh
./bin/pleroma_ctl emoji reload
```
This command only works with OTP releases.

View File

@ -0,0 +1,69 @@
# Managing frontends
`mix pleroma.frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>]`
Frontend can be installed either from local zip file, or automatically downloaded from the web.
You can give all the options directly on the command like, but missing information will be filled out by looking at the data configured under `frontends.available` in the config files.
Currently known `<frontend>` values are:
- [admin-fe](https://git.pleroma.social/pleroma/admin-fe)
- [kenoma](http://git.pleroma.social/lambadalambda/kenoma)
- [pleroma-fe](http://git.pleroma.social/pleroma/pleroma-fe)
- [fedi-fe](https://git.pleroma.social/pleroma/fedi-fe)
- [soapbox-fe](https://gitlab.com/soapbox-pub/soapbox-fe)
You can still install frontends that are not configured, see below.
## Example installations for a known frontend
For a frontend configured under the `available` key, it's enough to install it by name.
```sh tab="OTP"
./bin/pleroma_ctl frontend install pleroma
```
```sh tab="From Source"
mix pleroma.frontend install pleroma
```
This will download the latest build for the the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`).
You can override any of the details. To install a pleroma build from a different url, you could do this:
```sh tab="OPT"
./bin/pleroma_ctl frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
```
```sh tab="From Source"
mix pleroma.frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
```
Similarly, you can also install from a local zip file.
```sh tab="OTP"
./bin/pleroma_ctl frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip
```
```sh tab="From Source"
mix pleroma.frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip
```
The resulting frontend will always be installed into a folder of this template: `${instance_static}/frontends/${name}/${ref}`
Careful: This folder will be completely replaced on installation
## Example installation for an unknown frontend
The installation process is the same, but you will have to give all the needed options on the commond line. For example:
```sh tab="OTP"
./bin/pleroma_ctl frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip
```
```sh tab="From Source"
mix pleroma.frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip
```
If you don't have a zip file but just want to install a frontend from a local path, you can simply copy the files over a folder of this template: `${instance_static}/frontends/${name}/${ref}`

View File

@ -3,11 +3,15 @@
{! backend/administration/CLI_tasks/general_cli_task_info.include !} {! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Generate a new configuration file ## Generate a new configuration file
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl instance gen [option ...] ./bin/pleroma_ctl instance gen [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.instance gen [option ...] mix pleroma.instance gen [option ...]
``` ```

View File

@ -0,0 +1,20 @@
# Creating trusted OAuth App
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Create trusted OAuth App.
Optional params:
* `-s SCOPES` - scopes for app, e.g. `read,write,follow,push`.
=== "OTP"
```sh
./bin/pleroma_ctl app create -n APP_NAME -r REDIRECT_URI
```
=== "From Source"
```sh
mix pleroma.app create -n APP_NAME -r REDIRECT_URI
```

View File

@ -4,30 +4,42 @@
## Follow a relay ## Follow a relay
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl relay follow <relay_url> ./bin/pleroma_ctl relay follow <relay_url>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.relay follow <relay_url> mix pleroma.relay follow <relay_url>
``` ```
## Unfollow a remote relay ## Unfollow a remote relay
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl relay unfollow <relay_url> ./bin/pleroma_ctl relay unfollow <relay_url>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.relay unfollow <relay_url> mix pleroma.relay unfollow <relay_url>
``` ```
## List relay subscriptions ## List relay subscriptions
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl relay list ./bin/pleroma_ctl relay list
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.relay list mix pleroma.relay list
``` ```

View File

@ -0,0 +1,21 @@
# Managing robots.txt
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Generate a new robots.txt file and add it to the static directory
The `robots.txt` that ships by default is permissive. It allows well-behaved search engines to index all of your instance's URIs.
If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance [static directory](../../../configuration/static_dir/).
=== "OTP"
```sh
./bin/pleroma_ctl robots_txt disallow_all
```
=== "From Source"
```sh
mix pleroma.robots_txt disallow_all
```

View File

@ -3,11 +3,15 @@
{! backend/administration/CLI_tasks/general_cli_task_info.include !} {! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Migrate uploads from local to remote storage ## Migrate uploads from local to remote storage
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl uploads migrate_local <target_uploader> [option ...] ./bin/pleroma_ctl uploads migrate_local <target_uploader> [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.uploads migrate_local <target_uploader> [option ...] mix pleroma.uploads migrate_local <target_uploader> [option ...]
``` ```

View File

@ -4,11 +4,15 @@
## Create a user ## Create a user
```sh tab="OTP" === "OTP"
```sh
./bin/pleroma_ctl user new <nickname> <email> [option ...] ./bin/pleroma_ctl user new <nickname> <email> [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user new <nickname> <email> [option ...] mix pleroma.user new <nickname> <email> [option ...]
``` ```
@ -22,21 +26,31 @@ mix pleroma.user new <nickname> <email> [option ...]
- `-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
## List local users ## List local users
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user list ./bin/pleroma_ctl user list
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user list mix pleroma.user list
``` ```
## Generate an invite link ## Generate an invite link
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user invite [option ...] ./bin/pleroma_ctl user invite [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user invite [option ...] mix pleroma.user invite [option ...]
``` ```
@ -46,101 +60,166 @@ mix pleroma.user invite [option ...]
- `--max-use NUMBER` - maximum numbers of token uses - `--max-use NUMBER` - maximum numbers of token uses
## List generated invites ## List generated invites
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user invites ./bin/pleroma_ctl user invites
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user invites mix pleroma.user invites
``` ```
## Revoke invite ## Revoke invite
```sh tab="OTP"
./bin/pleroma_ctl user revoke_invite <token_or_id> === "OTP"
```sh
./bin/pleroma_ctl user revoke_invite <token>
``` ```
```sh tab="From Source" === "From Source"
mix pleroma.user revoke_invite <token_or_id>
```sh
mix pleroma.user revoke_invite <token>
``` ```
## Delete a user ## Delete a user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user rm <nickname> ./bin/pleroma_ctl user rm <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user rm <nickname> mix pleroma.user rm <nickname>
``` ```
## Delete user's posts and interactions ## Delete user's posts and interactions
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user delete_activities <nickname> ./bin/pleroma_ctl user delete_activities <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user delete_activities <nickname> mix pleroma.user delete_activities <nickname>
``` ```
## Sign user out from all applications (delete user's OAuth tokens and authorizations) ## Sign user out from all applications (delete user's OAuth tokens and authorizations)
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user sign_out <nickname> ./bin/pleroma_ctl user sign_out <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user sign_out <nickname> mix pleroma.user sign_out <nickname>
``` ```
## Deactivate or activate a user ## Deactivate or activate a user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user toggle_activated <nickname> ./bin/pleroma_ctl user toggle_activated <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user toggle_activated <nickname> mix pleroma.user toggle_activated <nickname>
``` ```
## Unsubscribe local users from a user and deactivate the user ## Deactivate a user and unsubscribes local users from the user
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe NICKNAME === "OTP"
```sh
./bin/pleroma_ctl user deactivate NICKNAME
``` ```
```sh tab="From Source" === "From Source"
mix pleroma.user unsubscribe NICKNAME
```sh
mix pleroma.user deactivate NICKNAME
``` ```
## Unsubscribe local users from an instance and deactivate all accounts on it ## Deactivate all accounts from an instance and unsubscribe local users on it
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance> === "OTP"
```sh
./bin/pleroma_ctl user deactivate_all_from_instance <instance>
``` ```
```sh tab="From Source" === "From Source"
mix pleroma.user unsubscribe_all_from_instance <instance>
```sh
mix pleroma.user deactivate_all_from_instance <instance>
``` ```
## Create a password reset link for user ## Create a password reset link for user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user reset_password <nickname> ./bin/pleroma_ctl user reset_password <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user reset_password <nickname> mix pleroma.user reset_password <nickname>
``` ```
## Disable Multi Factor Authentication (MFA/2FA) for a user
=== "OTP"
```sh
./bin/pleroma_ctl user reset_mfa <nickname>
```
=== "From Source"
```sh
mix pleroma.user reset_mfa <nickname>
```
## Set the value of the given user's settings ## Set the value of the given user's settings
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user set <nickname> [option ...] ./bin/pleroma_ctl user set <nickname> [option ...]
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user set <nickname> [option ...] mix pleroma.user set <nickname> [option ...]
``` ```
@ -150,31 +229,45 @@ mix pleroma.user set <nickname> [option ...]
- `--admin`/`--no-admin` - whether the user should be an admin - `--admin`/`--no-admin` - whether the user should be an admin
## Add tags to a user ## Add tags to a user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user tag <nickname> <tags> ./bin/pleroma_ctl user tag <nickname> <tags>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user tag <nickname> <tags> mix pleroma.user tag <nickname> <tags>
``` ```
## Delete tags from a user ## Delete tags from a user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user untag <nickname> <tags> ./bin/pleroma_ctl user untag <nickname> <tags>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user untag <nickname> <tags> mix pleroma.user untag <nickname> <tags>
``` ```
## Toggle confirmation status of the user ## Toggle confirmation status of the user
```sh tab="OTP"
=== "OTP"
```sh
./bin/pleroma_ctl user toggle_confirmed <nickname> ./bin/pleroma_ctl user toggle_confirmed <nickname>
``` ```
```sh tab="From Source" === "From Source"
```sh
mix pleroma.user toggle_confirmed <nickname> mix pleroma.user toggle_confirmed <nickname>
``` ```

View File

@ -18,9 +18,8 @@
6. Run `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>` 6. Run `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>`
7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. 7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
8. Restart the Pleroma service. 8. Restart the Pleroma service.
9. After you've restarted Pleroma, you will notice that postgres will take up more cpu resources than usual. A lot in fact. To fix this you must do a VACUUM ANLAYZE. This can also be done while the instance is still running like so: 9. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries.
$ sudo -u postgres psql pleroma_database_name
pleroma=# VACUUM ANALYZE;
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
## Remove ## Remove

View File

@ -1,6 +1,6 @@
# Updating your instance # Updating your instance
You should **always check the release notes/changelog** in case there are config deprecations, special update special update steps, etc. You should **always check the [release notes/changelog](https://git.pleroma.social/pleroma/pleroma/-/releases)** in case there are config deprecations, special update steps, etc.
Besides that, doing the following is generally enough: Besides that, doing the following is generally enough:

35
docs/ap_extensions.md Normal file
View File

@ -0,0 +1,35 @@
# ChatMessages
ChatMessages are the messages sent in 1-on-1 chats. They are similar to
`Note`s, but the addresing is done by having a single AP actor in the `to`
field. Addressing multiple actors is not allowed. These messages are always
private, there is no public version of them. They are created with a `Create`
activity.
Example:
```json
{
"actor": "http://2hu.gensokyo/users/raymoo",
"id": "http://2hu.gensokyo/objects/1",
"object": {
"attributedTo": "http://2hu.gensokyo/users/raymoo",
"content": "You expected a cute girl? Too bad.",
"id": "http://2hu.gensokyo/objects/2",
"published": "2020-02-12T14:08:20Z",
"to": [
"http://2hu.gensokyo/users/marisa"
],
"type": "ChatMessage"
},
"published": "2018-02-12T14:08:20Z",
"to": [
"http://2hu.gensokyo/users/marisa"
],
"type": "Create"
}
```
This setup does not prevent multi-user chats, but these will have to go through
a `Group`, which will be the recipient of the messages and then `Announce` them
to the users in the `Group`.

View File

@ -1,16 +1,16 @@
# Pleroma Clients # Pleroma Clients
Note: Additionnal clients may be working but theses are officially supporting Pleroma. Note: Additional clients may be working but theses are officially supporting Pleroma.
Feel free to contact us to be added to this list! Feel free to contact us to be added to this list!
## Desktop ## Desktop
### Roma for Desktop ### Roma for Desktop
- Homepage: <https://www.pleroma.com/#desktopApp> - Homepage: <https://www.pleroma.com/#desktopApp>
- Source Code: <https://github.com/roma-apps/roma-desktop> - Source Code: <https://github.com/roma-apps/roma-desktop>
- Platforms: Windows, Mac, (Linux?) - Platforms: Windows, Mac, Linux
- Features: Streaming Ready - Features: Streaming Ready
### Social ### Social
- Source Code: <https://gitlab.gnome.org/BrainBlasted/Social> - Source Code: <https://gitlab.gnome.org/World/Social>
- Contact: [@brainblasted@social.libre.fi](https://social.libre.fi/users/brainblasted) - Contact: [@brainblasted@social.libre.fi](https://social.libre.fi/users/brainblasted)
- Platforms: Linux (GNOME) - Platforms: Linux (GNOME)
- Note(2019-01-28): Not at a pre-alpha stage yet - Note(2019-01-28): Not at a pre-alpha stage yet
@ -42,18 +42,17 @@ Feel free to contact us to be added to this list!
- Platforms: SailfishOS - Platforms: SailfishOS
- Features: No Streaming - Features: No Streaming
### Nekonium ### Husky
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/) - Source code: <https://git.mentality.rip/FWGS/Husky>
- Source: <https://gogs.gdgd.jp.net/lin/nekonium> - Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)
- Contact: [@lin@pleroma.gdgd.jp.net](https://pleroma.gdgd.jp.net/users/lin)
- Platforms: Android - Platforms: Android
- Features: Streaming Ready - Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers
### Roma ### Fedi
- Homepage: <https://www.pleroma.com/#mobileApps> - Homepage: <https://www.fediapp.com/>
- Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android) - Source Code: Proprietary, but gratis
- Platforms: iOS, Android - Platforms: iOS, Android
- Features: No Streaming - Features: Pleroma-specific features like Reactions
### Tusky ### Tusky
- Homepage: <https://tuskyapp.github.io/> - Homepage: <https://tuskyapp.github.io/>
@ -64,9 +63,16 @@ Feel free to contact us to be added to this list!
### Twidere ### Twidere
- Homepage: <https://twidere.mariotaku.org/> - Homepage: <https://twidere.mariotaku.org/>
- Source Code: <https://github.com/TwidereProject/Twidere-Android/>, <https://github.com/TwidereProject/Twidere-iOS/> - Source Code: <https://github.com/TwidereProject/Twidere-Android/>
- Contact: <me@mariotaku.org> - Contact: <me@mariotaku.org>
- Platform: Android, iOS - Platform: Android
- Features: No Streaming
### Indigenous
- Homepage: <https://indigenous.realize.be/>
- Source Code: <https://github.com/swentel/indigenous-android/>
- Contact: [@realize.be@realize.be](@realize.be@realize.be)
- Platforms: Android
- Features: No Streaming - Features: No Streaming
## Alternative Web Interfaces ## Alternative Web Interfaces
@ -76,11 +82,6 @@ Feel free to contact us to be added to this list!
- Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc) - Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc)
- Features: No Streaming - Features: No Streaming
### Feather
- Source Code: <https://github.com/kaniini/feather>
- Contact: [@kaniini@pleroma.site](https://pleroma.site/kaniini)
- Features: No Streaming
### Halcyon ### Halcyon
- Source Code: <https://notabug.org/halcyon-suite/halcyon> - Source Code: <https://notabug.org/halcyon-suite/halcyon>
- Contact: [@halcyon@social.csswg.org](https://social.csswg.org/users/halcyon) - Contact: [@halcyon@social.csswg.org](https://social.csswg.org/users/halcyon)
@ -94,6 +95,15 @@ Feel free to contact us to be added to this list!
- Features: No Streaming - Features: No Streaming
### Sengi ### Sengi
- Homepage: <https://nicolasconstant.github.io/sengi/>
- Source Code: <https://github.com/NicolasConstant/sengi> - Source Code: <https://github.com/NicolasConstant/sengi>
- Contact: [@sengi_app@mastodon.social](https://mastodon.social/users/sengi_app) - Contact: [@sengi_app@mastodon.social](https://mastodon.social/users/sengi_app)
- Note(2019-01-28): The development is currently in a early stage.
### DashFE
- Source Code: <https://notabug.org/daisuke/DashboardFE>
- Contact: [@dashfe@stereophonic.space](https://stereophonic.space/users/dashfe)
### BloatFE
- Source Code: <https://git.freesoftwareextremist.com/bloat/>
- Contact: [@r@freesoftwareextremist.com](https://freesoftwareextremist.com/users/r)
- Features: Does not requires JavaScript

View File

@ -8,12 +8,17 @@ For from source installations Pleroma configuration works by first importing the
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted. To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :chat
* `enabled` - Enables the backend chat. Defaults to `true`.
## :instance ## :instance
* `name`: The instances name. * `name`: The instances name.
* `email`: Email used to reach an Administrator/Moderator of the instance. * `email`: Email used to reach an Administrator/Moderator of the instance.
* `notify_email`: Email used for notifications. * `notify_email`: Email used for notifications.
* `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``. * `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``.
* `limit`: Posts character limit (CW/Subject included in the counter). * `limit`: Posts character limit (CW/Subject included in the counter).
* `discription_limit`: The character limit for image descriptions.
* `chat_limit`: Character limit of the instance chat messages. * `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner). * `upload_limit`: File size limit of uploads (except for avatar, background, banner).
@ -28,36 +33,20 @@ To add configuration to your config file, you can copy it from the base config.
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false. * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`). * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in. * `account_activation_required`: Require users to confirm their emails before signing in.
* `account_approval_required`: Require users to be manually approved by an admin before signing in.
* `federating`: Enable federation with other instances. * `federating`: Enable federation with other instances.
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes. * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance. * `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance.
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default).
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt makes sense to use in production.
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private (DMs, followers-only) activities will not be send. * `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 [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
older software for theses nicknames. older software for theses nicknames.
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. * `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow. * `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses. * `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `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`). * `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`. * `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``. * `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
@ -70,14 +59,70 @@ To add configuration to your config file, you can copy it from the base config.
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`). * `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`).
* `account_field_name_length`: An account field name maximum length (default: `512`). * `account_field_name_length`: An account field name maximum length (default: `512`).
* `account_field_value_length`: An account field value maximum length (default: `2048`). * `account_field_value_length`: An account field value maximum length (default: `2048`).
* `registration_reason_length`: Maximum registration reason length (default: `500`).
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users. * `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. * `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
* `show_reactions`: Let favourites and emoji reactions be viewed through the API (default: `true`).
## Welcome
* `direct_message`: - welcome message sent as a direct message.
* `enabled`: Enables the send a direct message to a newly registered user. Defaults to `false`.
* `sender_nickname`: The nickname of the local user that sends the welcome message.
* `message`: A message that will be send to a newly registered users as a direct message.
* `chat_message`: - welcome message sent as a chat message.
* `enabled`: Enables the send a chat message to a newly registered user. Defaults to `false`.
* `sender_nickname`: The nickname of the local user that sends the welcome message.
* `message`: A message that will be send to a newly registered users as a chat message.
* `email`: - welcome message sent as a email.
* `enabled`: Enables the send a welcome email to a newly registered user. Defaults to `false`.
* `sender`: The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.
* `subject`: A subject of welcome email.
* `html`: A html that will be send to a newly registered users as a email.
* `text`: A text that will be send to a newly registered users as a email.
Example:
```elixir
config :pleroma, :welcome,
direct_message: [
enabled: true,
sender_nickname: "lain",
message: "Hi! Welcome on board!"
],
email: [
enabled: true,
sender: {"Pleroma App", "welcome@pleroma.app"},
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
```
## Message rewrite facility
### :mrf
* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default).
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt makes sense to use in production.
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.ActivityExpiration` to be enabled for processing the scheduled delections.
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
## Federation ## Federation
### MRF policies ### MRF policies
!!! note !!! note
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `rewrite_policy` under [:instance](#instance) section. Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `policies` under [:mrf](#mrf) section.
#### :mrf_simple #### :mrf_simple
* `media_removal`: List of instances to remove media from. * `media_removal`: List of instances to remove media from.
@ -85,6 +130,7 @@ To add configuration to your config file, you can copy it from the base config.
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline. * `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline.
* `reject`: List of instances to reject any activities from. * `reject`: List of instances to reject any activities from.
* `accept`: List of instances to accept any activities from. * `accept`: List of instances to accept any activities from.
* `followers_only`: List of instances to decrease post visibility to only the followers, including for DM mentions.
* `report_removal`: List of instances to reject reports from. * `report_removal`: List of instances to reject reports from.
* `avatar_removal`: List of instances to strip avatars from. * `avatar_removal`: List of instances to strip avatars from.
* `banner_removal`: List of instances to strip banners from. * `banner_removal`: List of instances to strip banners from.
@ -146,6 +192,15 @@ config :pleroma, :mrf_user_allowlist, %{
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely * `:reject` rejects the message entirely
#### :mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
#### :mrf_activity_expiration
* `days`: Default global expiration time for all local Create activities (in days)
### :activitypub ### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed * `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances * `outgoing_blocks`: Whether to federate blocks to other instances
@ -153,6 +208,11 @@ config :pleroma, :mrf_user_allowlist, %{
* `sign_object_fetches`: Sign object fetches with HTTP signatures * `sign_object_fetches`: Sign object fetches with HTTP signatures
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches * `authorized_fetch_mode`: Require HTTP signatures for AP fetches
## Pleroma.User
* `restricted_nicknames`: List of nicknames users may not register with.
* `email_blacklist`: List of email domains users may not register with.
## Pleroma.ScheduledActivity ## Pleroma.ScheduledActivity
* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`) * `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`)
@ -161,6 +221,8 @@ config :pleroma, :mrf_user_allowlist, %{
## Pleroma.ActivityExpiration ## Pleroma.ActivityExpiration
Enables the worker which processes posts scheduled for deletion. Pinned posts are exempt from expiration.
* `enabled`: whether expired activities will be sent to the job queue to be deleted * `enabled`: whether expired activities will be sent to the job queue to be deleted
## Frontends ## Frontends
@ -234,6 +296,7 @@ This section describe PWA manifest instance-specific values. Currently this opti
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`). * `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`).
## :emoji ## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]` * `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `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"]]` * `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"]]`
@ -242,10 +305,46 @@ This section describe PWA manifest instance-specific values. Currently this opti
memory for this amount of seconds multiplied by the number of files. memory for this amount of seconds multiplied by the number of files.
## :media_proxy ## :media_proxy
* `enabled`: Enables proxying of remote media to the instances proxy * `enabled`: Enables proxying of remote media to the instances 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. * `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)]`. * `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 * `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`)
* `invalidation`: options for remove media from cache after delete object:
* `enabled`: Enables purge cache
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
### Purge cache strategy
#### Pleroma.Web.MediaProxy.Invalidation.Script
This strategy allow perform external shell script to purge cache.
Urls of attachments pass to script as arguments.
* `script_path`: path to external script.
Example:
```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: "./installation/nginx-cache-purge.example"
```
#### Pleroma.Web.MediaProxy.Invalidation.Http
This strategy allow perform custom http request to purge cache.
* `method`: http method. default is `purge`
* `headers`: http headers.
* `options`: request options.
Example:
```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge,
headers: [],
options: []
```
## Link previews ## Link previews
@ -370,8 +469,7 @@ Available caches:
* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`) * `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`) * `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default` * `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `adapter`: array of hackney options * `adapter`: array of adapter options
### :hackney_pools ### :hackney_pools
@ -390,6 +488,37 @@ For each pool, the options are:
* `timeout` - retention duration for connections * `timeout` - retention duration for connections
### :connections_pool
*For `gun` adapter*
Settings for HTTP connection pool.
* `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries.
* `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart.
* `:max_connections` - Maximum number of connections in the pool.
* `:await_up_timeout` - Timeout to connect to the host.
* `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded.
### :pools
*For `gun` adapter*
Settings for request pools. These pools are limited on top of `:connections_pool`.
There are four pools used:
* `:federation` for the federation jobs. You may want this pool's max_connections to be at least equal to the number of federator jobs + retry queue jobs.
* `:media` - for rich media, media proxy.
* `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`.
* `:default` - for other requests.
For each pool, the options are:
* `:size` - limit to how much requests can be concurrently executed.
* `:timeout` - timeout while `gun` will wait for response
* `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped.
## Captcha ## Captcha
### Pleroma.Captcha ### Pleroma.Captcha
@ -407,7 +536,7 @@ A built-in captcha provider. Enabled by default.
#### Pleroma.Captcha.Kocaptcha #### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint, Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer. `https://captcha.kotobank.ch` is hosted by the developer.
* `endpoint`: the Kocaptcha endpoint to use. * `endpoint`: the Kocaptcha endpoint to use.
@ -415,21 +544,29 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
## Uploads ## Uploads
### Pleroma.Upload ### Pleroma.Upload
* `uploader`: Which one of the [uploaders](#uploaders) to use. * `uploader`: Which one of the [uploaders](#uploaders) to use.
* `filters`: List of [upload filters](#upload-filters) to use. * `filters`: List of [upload filters](#upload-filters) to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` * `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. * `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
* `default_description`: Sets which default description an image has if none is set explicitly. Options: nil (default) - Don't set a default, :filename - use the filename of the file, a string (e.g. "attachment") - Use this string
!!! warning !!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
### Uploaders ### Uploaders
#### Pleroma.Uploaders.Local #### Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory. * `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory.
#### Pleroma.Uploaders.S3 #### Pleroma.Uploaders.S3
Don't forget to configure [Ex AWS S3](#ex-aws-s3-settings)
* `bucket`: S3 bucket name. * `bucket`: S3 bucket name.
* `bucket_namespace`: S3 bucket namespace. * `bucket_namespace`: S3 bucket namespace.
* `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com") * `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com")
@ -438,17 +575,23 @@ For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint. At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems. * `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
#### Ex AWS S3 settings
* `access_key_id`: Access key ID
* `secret_access_key`: Secret access key
* `host`: S3 host
Example:
```elixir
config :ex_aws, :s3,
access_key_id: "xxxxxxxxxx",
secret_access_key: "yyyyyyyyyy",
host: "s3.eu-central-1.amazonaws.com"
```
### Upload filters ### Upload filters
#### Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
#### Pleroma.Upload.Filter.Dedupe
No specific configuration.
#### Pleroma.Upload.Filter.AnonymizeFilename #### Pleroma.Upload.Filter.AnonymizeFilename
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
@ -456,6 +599,20 @@ 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. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`. * `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`.
#### Pleroma.Upload.Filter.Dedupe
No specific configuration.
#### Pleroma.Upload.Filter.Exiftool
This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact.
No specific configuration.
#### Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
## Email ## Email
### Pleroma.Emails.Mailer ### Pleroma.Emails.Mailer
@ -516,8 +673,7 @@ Email notifications settings.
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage): Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
* `repo` - app's Ecto repo (`Pleroma.Repo`) * `repo` - app's Ecto repo (`Pleroma.Repo`)
* `verbose` - logs verbosity * `log` - logs verbosity
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
* `queues` - job queues (see below) * `queues` - job queues (see below)
* `crontab` - periodic jobs, see [`Oban.Cron`](#obancron) * `crontab` - periodic jobs, see [`Oban.Cron`](#obancron)
@ -581,24 +737,6 @@ config :pleroma, :workers,
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]` * `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
* deprecated options: `max_jobs`, `initial_timeout` * deprecated options: `max_jobs`, `initial_timeout`
### Pleroma.Scheduler
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
Example:
```elixir
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
timezone: :utc,
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
```
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
## :web_push_encryption, :vapid_details ## :web_push_encryption, :vapid_details
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it. Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
@ -720,10 +858,9 @@ or
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites" curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
``` ```
### :auth Warning: it's discouraged to use this feature because of the associated security risk: static / rarely changed instance-wide token is much weaker compared to email-password pair of a real admin user; consider using HTTP Basic Auth or OAuth-based authentication instead.
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator. ### :auth
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication.
Authentication / authorization settings. Authentication / authorization settings.
@ -754,6 +891,9 @@ Pleroma account will be created with the same name as the LDAP user name.
* `base`: LDAP base, e.g. "dc=example,dc=com" * `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" * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an
OpenLDAP server the value may be `uid: "uid"`.
### OAuth consumer mode ### OAuth consumer mode
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
@ -839,37 +979,115 @@ Configure OAuth 2 provider capabilities:
### :uri_schemes ### :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL. * `valid_schemes`: List of the scheme part that is considered valid to be an URL.
### :auto_linker ### Pleroma.Formatter
Configuration for the `auto_linker` library: Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear. * `class` - specify the class to be added to the generated link (default: `false`)
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear. * `rel` - specify the rel attribute (default: `ugc`)
* `new_window: true` - set to false to remove `target='_blank'` attribute. * `new_window` - adds `target="_blank"` attribute (default: `false`)
* `scheme: false` - Set to true to link urls with schema `http://google.com`. * `truncate` - Set to a number to truncate URLs longer then the number. Truncated URLs will end in `...` (default: `false`)
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`. * `strip_prefix` - Strip the scheme prefix (default: `false`)
* `strip_prefix: true` - Strip the scheme prefix. * `extra` - link URLs with rarely used schemes (magnet, ipfs, irc, etc.) (default: `true`)
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.). * `validate_tld` - Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for urls without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't) (default: `:no_scheme`)
Example: Example:
```elixir ```elixir
config :auto_linker, config :pleroma, Pleroma.Formatter,
opts: [
scheme: true,
extra: true,
class: false, class: false,
strip_prefix: false, rel: "ugc",
new_window: false, new_window: false,
rel: "ugc" truncate: false,
] strip_prefix: false,
extra: true,
validate_tld: :no_scheme
``` ```
## Custom Runtime Modules (`:modules`) ## Custom Runtime Modules (`:modules`)
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies). * `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
## :configurable_from_database ## :configurable_from_database
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information. Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
## :database_config_whitelist
List of valid configuration sections which are allowed to be configured from the
database. Settings stored in the database before the whitelist is configured are
still applied, so it is suggested to only use the whitelist on instances that
have not migrated the config to the database.
Example:
```elixir
config :pleroma, :database_config_whitelist, [
{:pleroma, :instance},
{:pleroma, Pleroma.Web.Metadata},
{:auto_linker}
]
```
### Multi-factor authentication - :two_factor_authentication
* `totp` - a list containing TOTP configuration
- `digits` - Determines the length of a one-time pass-code in characters. Defaults to 6 characters.
- `period` - a period for which the TOTP code will be valid in seconds. Defaults to 30 seconds.
* `backup_codes` - a list containing backup codes configuration
- `number` - number of backup codes to generate.
- `length` - backup code length. Defaults to 16 characters.
## Restrict entities access for unauthenticated users
### :restrict_unauthenticated
Restrict access for unauthenticated users to timelines (public and federated), user profiles and statuses.
* `timelines`: public and federated timelines
* `local`: public timeline
* `federated`: federated timeline (includes public timeline)
* `profiles`: user profiles
* `local`
* `remote`
* `activities`: statuses
* `local`
* `remote`
Note: when `:instance, :public` is set to `false`, all `:restrict_unauthenticated` items be effectively set to `true` by default. If you'd like to allow unauthenticated access to specific API endpoints on a private instance, please explicitly set `:restrict_unauthenticated` to non-default value in `config/prod.secret.exs`.
Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practical sense if `restrict_unauthenticated/timelines/federated` is set to `false` (since local public activities will still be delivered to unauthenticated users as part of federated timeline).
## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
## :instances_favicons
Control favicons for instances.
* `enabled`: Allow/disallow displaying and getting instances favicons
## Frontend management
Frontends in Pleroma are swappable - you can specify which one to use here.
You can set a frontends for the key `primary` and `admin` and the options of `name` and `ref`. This will then make Pleroma serve the frontend from a folder constructed by concatenating the instance static path, `frontends` and the name and ref.
The key `primary` refers to the frontend that will be served by default for general requests. The key `admin` refers to the frontend that will be served at the `/pleroma/admin` path.
If you don't set anything here, the bundled frontends will be used.
Example:
```
config :pleroma, :frontends,
primary: %{
"name" => "pleroma",
"ref" => "stable"
},
admin: %{
"name" => "admin",
"ref" => "develop"
}
```
This would serve the frontend from the the folder at `$instance_static/frontends/pleroma/stable`. You have to copy the frontend into this folder yourself. You can choose the name and ref any way you like, but they will be used by mix tasks to automate installation in the future, the name referring to the project and the ref referring to a commit.

View File

@ -0,0 +1,153 @@
# How to activate Pleroma in-database configuration
## Explanation
The configuration of Pleroma has traditionally been managed with a config file, e.g. `config/prod.secret.exs`. This method requires a restart of the application for any configuration changes to take effect. We have made it possible to control most settings in the AdminFE interface after running a migration script.
## Migration to database config
1. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened.
**Source:**
```
$ mix pleroma.config migrate_to_db
```
or
**OTP:**
*Note: OTP users need Pleroma to be running for `pleroma_ctl` commands to work*
```
$ ./bin/pleroma_ctl config migrate_to_db
```
```
10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Migrating settings from file: /home/pleroma/config/dev.secret.exs
10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms
TRUNCATE config; []
10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms
ALTER SEQUENCE config_id_seq RESTART; []
10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"]
10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms
INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]]
Settings for key instance migrated.
Settings for group :pleroma migrated.
```
2. It is recommended to backup your config file now.
```
cp config/dev.secret.exs config/dev.secret.exs.orig
```
3. Edit your Pleroma config to enable database configuration:
```
config :pleroma, configurable_from_database: true
```
4. ⚠️ **THIS IS NOT REQUIRED** ⚠️
Now you can edit your config file and strip it down to the only settings which are not possible to control in the database. e.g., the Postgres (Repo) and webserver (Endpoint) settings cannot be controlled in the database because the application needs the settings to start up and access the database.
Any settings in the database will override those in the config file, but you may find it less confusing if the setting is only declared in one place.
A non-exhaustive list of settings that are only possible in the config file include the following:
* config :pleroma, Pleroma.Web.Endpoint
* config :pleroma, Pleroma.Repo
* config :pleroma, configurable\_from\_database
* config :pleroma, :database, rum_enabled
* config :pleroma, :connections_pool
Here is an example of a server config stripped down after migration:
```
use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "pleroma",
password: "MySecretPassword",
database: "pleroma_prod",
hostname: "localhost"
config :pleroma, configurable_from_database: true
```
5. Restart your instance and you can now access the Settings tab in AdminFE.
## Reverting back from database config
1. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened.
**Source:**
```
$ mix pleroma.config migrate_from_db
```
or
**OTP:**
```
$ ./bin/pleroma_ctl config migrate_from_db
```
```
10:26:30.593 [debug] QUERY OK source="config" db=9.8ms decode=1.2ms queue=26.0ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
10:26:30.659 [debug] QUERY OK source="config" db=1.1ms idle=80.7ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Database configuration settings have been saved to config/dev.exported_from_db.secret.exs
```
2. Remove `config :pleroma, configurable_from_database: true` from your config. The in-database configuration still exists, but it will not be used. Future migrations will erase the database config before importing your config file again.
3. Restart your instance.
## Debugging
### Clearing database config
You can clear the database config by truncating the `config` table in the database. e.g.,
```
psql -d pleroma_dev
pleroma_dev=# TRUNCATE config;
TRUNCATE TABLE
```
Additionally, every time you migrate the configuration to the database the config table is automatically truncated to ensure a clean migration.
### Manually removing a setting
If you encounter a situation where the server cannot run properly because of an invalid setting in the database and this is preventing you from accessing AdminFE, you can manually remove the offending setting if you know which one it is.
e.g., here is an example showing a minimal configuration in the database. Only the `config :pleroma, :instance` settings are in the table:
```
psql -d pleroma_dev
pleroma_dev=# select * from config;
id | key | value | inserted_at | updated_at | group
----+-----------+------------------------------------------------------------+---------------------+---------------------+----------
1 | :instance | \x836c0000000168026400046e616d656d00000007426c65726f6d616a | 2020-07-12 15:33:29 | 2020-07-12 15:33:29 | :pleroma
(1 row)
pleroma_dev=# delete from config where key = ':instance' and group = ':pleroma';
DELETE 1
```
Now the `config :pleroma, :instance` settings have been removed from the database.

View File

@ -60,7 +60,7 @@ Example of `my-awesome-theme.json` where we add the name "My Awesome Theme"
### Set as default theme ### Set as default theme
Now we can set the new theme as default in the [Pleroma FE configuration](General-tips-for-customizing-Pleroma-FE.md). Now we can set the new theme as default in the [Pleroma FE configuration](../../../frontend/CONFIGURATION).
Example of adding the new theme in the back-end config files Example of adding the new theme in the back-end config files
```elixir ```elixir

View File

@ -34,18 +34,22 @@ config :pleroma, :instance,
To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this: To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this:
```elixir ```elixir
config :pleroma, :instance, config :pleroma, :mrf,
[...] [...]
rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy policies: Pleroma.Web.ActivityPub.MRF.SimplePolicy
``` ```
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are: Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `reject`: Servers in this group will have their messages rejected. * `reject`: Servers in this group will have their messages rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields. * `accept`: If not empty, only messages from these instances will be accepted (whitelist federation).
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `avatar_removal`: Avatars from these servers will be stripped from incoming messages.
* `banner_removal`: Banner images from these servers will be stripped from incoming messages.
* `report_removal`: Servers in this group will have their reports (flags) rejected. * `report_removal`: Servers in this group will have their reports (flags) rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
* `reject_deletes`: Deletion requests will be rejected from these servers.
Servers should be configured as lists. Servers should be configured as lists.
@ -54,8 +58,8 @@ Servers should be configured as lists.
This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`: This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`:
```elixir ```elixir
config :pleroma, :instance, config :pleroma, :mrf,
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
config :pleroma, :mrf_simple, config :pleroma, :mrf_simple,
media_removal: ["illegalporn.biz"], media_removal: ["illegalporn.biz"],
@ -71,7 +75,7 @@ The effects of MRF policies can be very drastic. It is important to use this fun
## Writing your own MRF Policy ## Writing your own MRF Policy
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting. As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting.
For example, here is a sample policy module which rewrites all messages to "new message content": For example, here is a sample policy module which rewrites all messages to "new message content":
@ -113,7 +117,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RewritePolicy do
@impl true @impl true
def describe do def describe do
{:ok, %{mrf_sample: %{content: "new message content"}}}` {:ok, %{mrf_sample: %{content: "new message content"}}}
end end
end end
``` ```
@ -121,8 +125,8 @@ end
If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so: If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so:
```elixir ```elixir
config :pleroma, :instance, config :pleroma, :mrf,
rewrite_policy: [ policies: [
Pleroma.Web.ActivityPub.MRF.SimplePolicy, Pleroma.Web.ActivityPub.MRF.SimplePolicy,
Pleroma.Web.ActivityPub.MRF.RewritePolicy Pleroma.Web.ActivityPub.MRF.RewritePolicy
] ]

View File

@ -0,0 +1,31 @@
# Optimizing your PostgreSQL performance
Pleroma performance depends to a large extent on good database performance. The default PostgreSQL settings are mostly fine, but often you can get better performance by changing a few settings.
You can use [PGTune](https://pgtune.leopard.in.ua) to get recommendations for your setup. If you do, set the "Number of Connections" field to 20, as Pleroma will only use 10 concurrent connections anyway. If you don't, it will give you advice that might even hurt your performance.
We also recommend not using the "Network Storage" option.
## Example configurations
Here are some configuration suggestions for PostgreSQL 10+.
### 1GB RAM, 1 CPU
```
shared_buffers = 256MB
effective_cache_size = 768MB
maintenance_work_mem = 64MB
work_mem = 13107kB
```
### 2GB RAM, 2 CPU
```
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
work_mem = 26214kB
max_worker_processes = 2
max_parallel_workers_per_gather = 1
max_parallel_workers = 2
```

View File

@ -1,45 +1,61 @@
# Static Directory # Static Directory
Static frontend files are shipped in `priv/static/` and tracked by version control in this repository. If you want to overwrite or update these without the possibility of merge conflicts, you can write your custom versions to `instance/static/`. Static frontend files are shipped with pleroma. If you want to overwrite or update these without problems during upgrades, you can write your custom versions to the static directory.
``` You can find the location of the static directory in the [configuration](../cheatsheet/#instance).
=== "OTP"
```elixir
config :pleroma, :instance, config :pleroma, :instance,
static_dir: "instance/static/", static_dir: "/var/lib/pleroma/static/"
``` ```
For example, edit `instance/static/instance/panel.html` . === "From Source"
```elixir
config :pleroma, :instance,
static_dir: "instance/static/"
```
Alternatively, you can overwrite this value in your configuration to use a different static instance directory. Alternatively, you can overwrite this value in your configuration to use a different static instance directory.
This document is written assuming `instance/static/`. This document is written using `$static_dir` as the value of the `config :pleroma, :instance, static_dir` setting.
Or, if you want to manage your custom file in git repository, basically remove the `instance/` entry from `.gitignore`. If you use a From Source installation and want to manage your custom files in the git repository, you can remove the `instance/` entry from `.gitignore`.
## robots.txt ## robots.txt
By default, the `robots.txt` that ships in `priv/static/` is permissive. It allows well-behaved search engines to index all of your instance's URIs. There's a mix tasks to [generate a new robot.txt](../../administration/CLI_tasks/robots_txt/).
If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance static directory. For more complex things, you can write your own robots.txt to `$static_dir/robots.txt`.
E.g. if you want to block all crawlers except for [fediverse.network](https://fediverse.network/about) you can use
``` ```
mix pleroma.robots_txt disallow_all User-Agent: *
Disallow: /
User-Agent: crawler-us-il-1.fediverse.network
Allow: /
User-Agent: makhnovtchina.random.sh
Allow: /
``` ```
## Thumbnail ## Thumbnail
Put on `instance/static/instance/thumbnail.jpeg` with your selfie or other neat picture. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html). Add `$static_dir/instance/thumbnail.jpeg` with your selfie or other neat picture. It will be available on `http://your-domain.tld/instance/thumbnail.jpeg` and can be used by external applications.
## Instance-specific panel ## Instance-specific panel
![instance-specific panel demo](/uploads/296b19ec806b130e0b49b16bfe29ce8a/image.png) Create and Edit your file at `$static_dir/instance/panel.html`.
Create and Edit your file on `instance/static/instance/panel.html`.
## Background ## Background
You can change the background of your Pleroma instance by uploading it to `instance/static/`, and then changing `background` in `config/prod.secret.exs` accordingly. You can change the background of your Pleroma instance by uploading it to `$static_dir/`, and then changing `background` in [your configuration](../cheatsheet/#frontend_configurations) accordingly.
If you put `instance/static/images/background.jpg` E.g. if you put `$static_dir/images/background.jpg`
``` ```
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
@ -50,12 +66,14 @@ config :pleroma, :frontend_configurations,
## Logo ## Logo
![logo modification demo](/uploads/c70b14de60fa74245e7f0dcfa695ebff/image.png) !!! important
Note the extra `static` folder for the default logo.png location
If you want to give a brand to your instance, You can change the logo of your instance by uploading it to `instance/static/`. If you want to give a brand to your instance, You can change the logo of your instance by uploading it to the static directory `$static_dir/static/logo.png`.
Alternatively, you can specify the path with config. Alternatively, you can specify the path to your logo in [your configuration](../cheatsheet/#frontend_configurations).
If you put `instance/static/static/mylogo-file.png`
E.g. if you put `$static_dir/static/mylogo-file.png`
``` ```
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
@ -66,4 +84,7 @@ config :pleroma, :frontend_configurations,
## Terms of Service ## Terms of Service
Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by changing `instance/static/static/terms-of-service.html`. !!! important
Note the extra `static` folder for the terms-of-service.html
Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by adding and changing `$static_dir/static/terms-of-service.html`.

View File

@ -0,0 +1,38 @@
# Storing Remote Media
Pleroma does not store remote/federated media by default. The best way to achieve this is to change Nginx to keep its reverse proxy cache
for a year and to activate the `MediaProxyWarmingPolicy` MRF policy in Pleroma which will automatically fetch all media through the proxy
as soon as the post is received by your instance.
## Nginx
```
proxy_cache_path /long/term/storage/path/pleroma-media-cache levels=1:2
keys_zone=pleroma_media_cache:10m inactive=1y use_temp_path=off;
location ~ ^/(media|proxy) {
proxy_cache pleroma_media_cache;
slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_http_version 1.1;
proxy_cache_valid 206 301 302 304 1h;
proxy_cache_valid 200 1y;
proxy_cache_use_stale error timeout invalid_header updating;
proxy_ignore_client_abort on;
proxy_buffering on;
chunked_transfer_encoding on;
proxy_ignore_headers Cache-Control Expires;
proxy_hide_header Cache-Control Expires;
proxy_pass http://127.0.0.1:4000;
}
```
## Pleroma
Add to your `prod.secret.exs`:
```
config :pleroma, :mrf,
policies: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
```

23
docs/dev.md Normal file
View File

@ -0,0 +1,23 @@
This document contains notes and guidelines for Pleroma developers.
# Authentication & Authorization
## OAuth token-based authentication & authorization
* Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes. For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/).
* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`.
* In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition) be called prior to actual controller action, and it'll perform security / privacy checks before passing control to actual controller action.
For routes with `:authenticated_api` pipeline, authentication & authorization are expected, thus `OAuthScopesPlug` will be run unless explicitly skipped (also `EnsureAuthenticatedPlug` will be executed immediately before action even if there was an early run to give an early error, since `OAuthScopesPlug` supports `:proceed_unauthenticated` option, and other plugs may support similar options as well).
For `:api` pipeline routes, it'll be verified whether `OAuthScopesPlug` was called or explicitly skipped, and if it was not then auth information will be dropped for request. Then `EnsurePublicOrAuthenticatedPlug` will be called to ensure that either the instance is not private or user is authenticated (unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy for users.
## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided.
## Auth-related configuration, OAuth consumer mode etc.
See `Authentication` section of [the configuration cheatsheet](configuration/cheatsheet.md#authentication).

26
docs/index.md Normal file
View File

@ -0,0 +1,26 @@
# Introduction to Pleroma
## What is Pleroma?
Pleroma is a federated social networking platform, compatible with Mastodon and other 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 an instance is enough to talk to the entire fediverse!
## How can I use it?
Pleroma instances are already widely deployed, a list can be found at <https://the-federation.info/pleroma> and <https://fediverse.network/pleroma>.
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
Installation instructions can be found in the installation section of these docs.
## I got an account, now what?
Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register)
### Pleroma-FE
The default front-end used by Pleroma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](../frontend).
### 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!
Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.

View File

@ -14,6 +14,7 @@ It assumes that you have administrative rights, either as root or a user with [s
* `erlang-xmerl` * `erlang-xmerl`
* `git` * `git`
* Development Tools * Development Tools
* `cmake`
#### Optional packages used in this guide #### Optional packages used in this guide
@ -39,7 +40,7 @@ sudo apk upgrade
* Install some tools, which are needed later: * Install some tools, which are needed later:
```shell ```shell
sudo apk add git build-base sudo apk add git build-base cmake
``` ```
### Install Elixir and Erlang ### Install Elixir and Erlang
@ -225,10 +226,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## Questions ## Questions

View File

@ -9,6 +9,7 @@ This guide will assume that you have administrative rights, either as root or a
* `elixir` * `elixir`
* `git` * `git`
* `base-devel` * `base-devel`
* `cmake`
#### Optional packages used in this guide #### Optional packages used in this guide
@ -26,7 +27,7 @@ sudo pacman -Syu
* Install some of the above mentioned programs: * Install some of the above mentioned programs:
```shell ```shell
sudo pacman -S git base-devel elixir sudo pacman -S git base-devel elixir cmake
``` ```
### Install PostgreSQL ### Install PostgreSQL
@ -200,10 +201,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## Questions ## Questions

View File

@ -7,15 +7,12 @@ This guide will assume you are on Debian Stretch. This guide should also work wi
* `postgresql` (9.6+, Ubuntu 16.04 comes with 9.5, you can get a newer version from [here](https://www.postgresql.org/download/linux/ubuntu/)) * `postgresql` (9.6+, Ubuntu 16.04 comes with 9.5, you can get a newer version from [here](https://www.postgresql.org/download/linux/ubuntu/))
* `postgresql-contrib` (9.6+, same situtation as above) * `postgresql-contrib` (9.6+, same situtation as above)
* `elixir` (1.5+, [install from here, Debian and Ubuntu ship older versions](https://elixir-lang.org/install.html#unix-and-unix-like) or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user) * `elixir` (1.8+, Follow the guide to install from the Erlang Solutions repo or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user)
* `erlang-dev` * `erlang-dev`
* `erlang-tools` * `erlang-nox`
* `erlang-parsetools`
* `erlang-eldap`, if you want to enable ldap authenticator
* `erlang-ssh`
* `erlang-xmerl`
* `git` * `git`
* `build-essential` * `build-essential`
* `cmake`
#### Optional packages used in this guide #### Optional packages used in this guide
@ -34,7 +31,7 @@ sudo apt full-upgrade
* Install some of the above mentioned programs: * Install some of the above mentioned programs:
```shell ```shell
sudo apt install git build-essential postgresql postgresql-contrib sudo apt install git build-essential postgresql postgresql-contrib cmake
``` ```
### Install Elixir and Erlang ### Install Elixir and Erlang
@ -42,15 +39,15 @@ sudo apt install git build-essential postgresql postgresql-contrib
* Download and add the Erlang repository: * Download and add the Erlang repository:
```shell ```shell
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
``` ```
* Install Elixir and Erlang: * Install Elixir and Erlang:
```shell ```shell
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh sudo apt install elixir erlang-dev erlang-nox
``` ```
### Install PleromaBE ### Install PleromaBE
@ -190,10 +187,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## Questions ## Questions

View File

@ -10,21 +10,18 @@
### 必要なソフトウェア ### 必要なソフトウェア
- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) - PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
- postgresql-contrib 9.6以上 (同上) - `postgresql-contrib` 9.6以上 (同上)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) - Elixir 1.8 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- erlang-dev - `erlang-dev`
- erlang-tools - `erlang-nox`
- erlang-parsetools - `git`
- erlang-eldap (LDAP認証を有効化するときのみ必要) - `build-essential`
- erlang-ssh - `cmake`
- erlang-xmerl
- git
- build-essential
#### このガイドで利用している追加パッケージ #### このガイドで利用している追加パッケージ
- nginx (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください) - `nginx` (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください)
- certbot (または何らかのLet's Encrypt向けACMEクライアント) - `certbot` (または何らかのLet's Encrypt向けACMEクライアント)
### システムを準備する ### システムを準備する
@ -36,7 +33,7 @@ sudo apt full-upgrade
* 上記に挙げたパッケージをインストールしておきます。 * 上記に挙げたパッケージをインストールしておきます。
``` ```
sudo apt install git build-essential postgresql postgresql-contrib sudo apt install git build-essential postgresql postgresql-contrib cmake
``` ```
@ -44,14 +41,14 @@ sudo apt install git build-essential postgresql postgresql-contrib
* Erlangのリポジトリをダウンロードおよびインストールします。 * Erlangのリポジトリをダウンロードおよびインストールします。
``` ```
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
``` ```
* ElixirとErlangをインストールします、 * ElixirとErlangをインストールします、
``` ```
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh sudo apt install elixir erlang-dev erlang-nox
``` ```
### Pleroma BE (バックエンド) をインストールします ### Pleroma BE (バックエンド) をインストールします
@ -179,10 +176,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### その他の設定とカスタマイズ #### その他の設定とカスタマイズ
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## 質問ある? ## 質問ある?

View File

@ -0,0 +1,210 @@
# Installing on FreeBSD
This document was written for FreeBSD 12.1, but should be work on future releases.
## Required software
This assumes the target system has `pkg(8)`.
```
# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh
```
Copy the rc.d scripts to the right directory:
Setup the required services to automatically start at boot, using `sysrc(8)`.
```
# sysrc nginx_enable=YES
# sysrc postgresql_enable=YES
```
## Initialize postgres
```
# service postgresql initdb
# service postgresql start
```
## Configuring Pleroma
Create a user for Pleroma:
```
# pw add user pleroma -m
# echo 'export LC_ALL="en_US.UTF-8"' >> /home/pleroma/.profile
# su -l pleroma
```
Clone the repository:
```
$ cd $HOME # Should be the same as /home/pleroma
$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git
```
Configure Pleroma. Note that you need a domain name at this point:
```
$ cd /home/pleroma/pleroma
$ mix deps.get # Enter "y" when asked to install Hex
$ mix pleroma.instance gen # You will be asked a few questions here.
$ cp config/generated_config.exs config/prod.secret.exs
```
Since Postgres is configured, we can now initialize the database. There should
now be a file in `config/setup_db.psql` that makes this easier. Edit it, and
*change the password* to a password of your choice. Make sure it is secure, since
it'll be protecting your database. As root, you can now initialize the database:
```
# cd /home/pleroma/pleroma
# sudo -Hu postgres -g postgres psql -f config/setup_db.psql
```
Postgres allows connections from all users without a password by default. To
fix this, edit `/var/db/postgres/data12/pg_hba.conf`. Change every `trust` to
`password`.
Once this is done, restart Postgres with:
```
# service postgresql restart
```
Run the database migrations.
Back as the pleroma user, run the following to implement any database migrations.
```
# su -l pleroma
$ cd /home/pleroma/pleroma
$ MIX_ENV=prod mix ecto.migrate
```
You will need to do this whenever you update with `git pull`:
## Configuring acme.sh
We'll be using acme.sh in Stateless Mode for TLS certificate renewal.
First, as root, allow the user `acme` to have access to the acme log file, as follows:
```
# touch /var/log/acme.sh.log
# chown acme:acme /var/log/acme.sh.log
# chmod 600 /var/log/acme.sh.log
```
Next, obtain your account fingerprint:
```
# sudo -Hu acme -g acme acme.sh --register-account
```
You need to add the following to your nginx configuration for the server
running on port 80:
```
location ~ ^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$ {
default_type text/plain;
return 200 "$1.6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd";
}
```
Replace the string after after `$1.` with your fingerprint.
Start nginx:
```
# service nginx start
```
It should now be possible to issue a cert (replace `example.com`
with your domain name):
```
# sudo -Hu acme -g acme acme.sh --issue -d example.com --stateless
```
Let's add auto-renewal to `/etc/crontab`
(replace `example.com` with your domain):
```
/usr/local/bin/sudo -Hu acme -g acme /usr/local/sbin/acme.sh -r -d example.com --stateless
```
### Configuring nginx
FreeBSD's default nginx configuration does not contain an include directive, which is
typically used for multiple sites. Therefore, you will need to first create the required
directory as follows:
```
# mkdir -p /usr/local/etc/nginx/sites-available
```
Next, add an `include` directive to `/usr/local/etc/nginx/nginx.conf`, within the `http {}`
block, as follows:
```
http {
...
include /usr/local/etc/nginx/sites-available/*;
}
```
As root, copy `/home/pleroma/pleroma/installation/pleroma.nginx` to
`/usr/local/etc/nginx/sites-available/pleroma.nginx`.
Edit the defaults of `/usr/local/etc/nginx/sites-available/pleroma.nginx`:
* Change `ssl_trusted_certificate` to `/var/db/acme/certs/example.tld/example.tld.cer`.
* Change `ssl_certificate` to `/var/db/acme/certs/example.tld/fullchain.cer`.
* Change `ssl_certificate_key` to `/var/db/acme/certs/example.tld/example.tld.key`.
* Change all references of `example.tld` to your instance's domain name.
## Creating a startup script for Pleroma
Pleroma will need to compile when it initially starts, which typically takes a longer
period of time. Therefore, it is good practice to initially run pleroma from the
command-line before utilizing the rc.d script. That is done as follows:
```
# su -l pleroma
$ cd $HOME/pleroma
$ MIX_ENV=prod mix phx.server
```
Copy the startup script to the correct location and make sure it's executable:
```
# cp /home/pleroma/pleroma/installation/freebsd/rc.d/pleroma /usr/local/etc/rc.d/pleroma
# chmod +x /usr/local/etc/rc.d/pleroma
```
Update the `/etc/rc.conf` and start pleroma with the following commands:
```
# sysrc pleroma_enable=YES
# service 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
sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
```
## Conclusion
Restart nginx with `# service nginx restart` and you should be up and running.
Make sure your time is in sync, or other instances will receive your posts with
incorrect timestamps. You should have ntpd running.
## Questions
Questions about the installation or didnt 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**.

View File

@ -0,0 +1,5 @@
* [How Federation Works/Why is my Federated Timeline empty?](https://blog.soykaf.com/post/how-federation-works/)
* [Backup your instance](../administration/backup.md)
* [Updating your instance](../administration/updating.md)
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)

View File

@ -28,6 +28,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
* `dev-db/postgresql` * `dev-db/postgresql`
* `dev-lang/elixir` * `dev-lang/elixir`
* `dev-vcs/git` * `dev-vcs/git`
* `dev-util/cmake`
#### Optional ebuilds used in this guide #### Optional ebuilds used in this guide
@ -46,7 +47,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
* Emerge all required the required and suggested software in one go: * Emerge all required the required and suggested software in one go:
```shell ```shell
# emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake
``` ```
If you would not like to install the optional packages, remove them from this line. If you would not like to install the optional packages, remove them from this line.
@ -283,10 +284,7 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
#### Further reading #### Further reading
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## Questions ## Questions

View File

@ -8,11 +8,13 @@ You will be running commands as root. If you aren't root already, please elevate
The system needs to have `curl` and `unzip` installed for downloading and unpacking release builds. The system needs to have `curl` and `unzip` installed for downloading and unpacking release builds.
```sh tab="Alpine" === "Alpine"
```sh
apk add curl unzip apk add curl unzip
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```sh
apt install curl unzip apt install curl unzip
``` ```
@ -110,7 +112,8 @@ OTP releases have different service files than from-source installs so they need
**Warning:** The service files assume pleroma user's home directory is `/opt/pleroma`, please make sure all paths fit your installation. **Warning:** The service files assume pleroma user's home directory is `/opt/pleroma`, please make sure all paths fit your installation.
```sh tab="Alpine" === "Alpine"
```sh
# Copy the service into a proper directory # Copy the service into a proper directory
cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma
@ -118,7 +121,8 @@ cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma
rc-service pleroma start rc-service pleroma start
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```sh
# Copy the service into a proper directory # Copy the service into a proper directory
cp ~pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service cp ~pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service

View File

@ -19,6 +19,7 @@ databases/postgresql11-client
databases/postgresql11-server databases/postgresql11-server
devel/git-base devel/git-base
devel/git-docs devel/git-docs
devel/cmake
lang/elixir lang/elixir
security/acmesh security/acmesh
security/sudo security/sudo
@ -196,3 +197,11 @@ incorrect timestamps. You should have ntpd running.
## Instances running NetBSD ## Instances running NetBSD
* <https://catgirl.science> * <https://catgirl.science>
#### Further reading
{! backend/installation/further_reading.include !}
## Questions
Questions about the installation or didnt 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**.

View File

@ -14,11 +14,12 @@ The following packages need to be installed:
* git * git
* postgresql-server * postgresql-server
* postgresql-contrib * postgresql-contrib
* cmake
To install them, run the following command (with doas or as root): To install them, run the following command (with doas or as root):
``` ```
pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib cmake
``` ```
Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt.
@ -242,3 +243,11 @@ If your instance is up and running, you can create your first user with administ
``` ```
LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
``` ```
#### Further reading
{! backend/installation/further_reading.include !}
## Questions
Questions about the installation or didnt 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**.

View File

@ -16,7 +16,7 @@ Matrix-kanava #freenode_#pleroma:matrix.org ovat hyviä paikkoja löytää apua
Asenna tarvittava ohjelmisto: Asenna tarvittava ohjelmisto:
`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3` `# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake`
Luo postgresql-tietokanta: Luo postgresql-tietokanta:

View File

@ -28,13 +28,15 @@ Other than things bundled in the OTP release Pleroma depends on:
* nginx (could be swapped with another reverse proxy but this guide covers only it) * nginx (could be swapped with another reverse proxy but this guide covers only it)
* certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it)
```sh tab="Alpine" === "Alpine"
```
echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories
apk update apk update
apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot
``` ```
@ -47,7 +49,8 @@ apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results). RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results).
```sh tab="Alpine" === "Alpine"
```
apk add git build-base postgresql-dev apk add git build-base postgresql-dev
git clone https://github.com/postgrespro/rum /tmp/rum git clone https://github.com/postgrespro/rum /tmp/rum
cd /tmp/rum cd /tmp/rum
@ -57,19 +60,22 @@ cd
rm -r /tmp/rum rm -r /tmp/rum
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
# Available only on Buster/19.04 # Available only on Buster/19.04
apt install postgresql-11-rum apt install postgresql-11-rum
``` ```
#### (Optional) Performance configuration #### (Optional) Performance configuration
For optimal performance, you may use [PGTune](https://pgtune.leopard.in.ua), don't forget to restart postgresql after editing the configuration It is encouraged to check [Optimizing your PostgreSQL performance](../configuration/postgresql.md) document, for tips on PostgreSQL tuning.
```sh tab="Alpine" === "Alpine"
```
rc-service postgresql restart rc-service postgresql restart
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
systemctl restart postgresql systemctl restart postgresql
``` ```
@ -151,11 +157,13 @@ certbot certonly --standalone --preferred-challenges http -d yourinstance.tld
The location of nginx configs is dependent on the distro The location of nginx configs is dependent on the distro
```sh tab="Alpine" === "Alpine"
```
cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.conf cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.conf
ln -s /etc/nginx/sites-available/pleroma.conf /etc/nginx/sites-enabled/pleroma.conf ln -s /etc/nginx/sites-available/pleroma.conf /etc/nginx/sites-enabled/pleroma.conf
``` ```
@ -175,11 +183,13 @@ nginx -t
``` ```
#### Start nginx #### Start nginx
```sh tab="Alpine" === "Alpine"
```
rc-service nginx start rc-service nginx start
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
systemctl start nginx systemctl start nginx
``` ```
@ -187,7 +197,8 @@ At this point if you open your (sub)domain in a browser you should see a 502 err
### Setting up a system service ### Setting up a system service
```sh tab="Alpine" === "Alpine"
```
# Copy the service into a proper directory # Copy the service into a proper directory
cp /opt/pleroma/installation/init.d/pleroma /etc/init.d/pleroma cp /opt/pleroma/installation/init.d/pleroma /etc/init.d/pleroma
@ -196,7 +207,8 @@ rc-service pleroma start
rc-update add pleroma rc-update add pleroma
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
# Copy the service into a proper directory # Copy the service into a proper directory
cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
@ -223,7 +235,8 @@ $EDITOR path-to-nginx-config
nginx -t nginx -t
``` ```
```sh tab="Alpine" === "Alpine"
```
# Restart nginx # Restart nginx
rc-service nginx restart rc-service nginx restart
@ -244,7 +257,8 @@ chmod +x /etc/periodic/daily/renew-pleroma-cert
run-parts --test /etc/periodic/daily run-parts --test /etc/periodic/daily
``` ```
```sh tab="Debian/Ubuntu" === "Debian/Ubuntu"
```
# Restart nginx # Restart nginx
systemctl restart nginx systemctl restart nginx
@ -270,10 +284,7 @@ This will create an account withe the username of 'joeuser' with the email addre
## Further reading ## Further reading
* [Backup your instance](../administration/backup.md) {! backend/installation/further_reading.include !}
* [Hardening your instance](../configuration/hardening.md)
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Updating your instance](../administration/updating.md)
## Questions ## Questions

View File

@ -1,65 +0,0 @@
# Introduction to 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 an instance is enough to talk to the entire fediverse!
## How can I use it?
Pleroma instances are already widely deployed, a list can be found at <http://distsn.org/pleroma-instances.html>. Information on all existing fediverse instances can be found at <https://fediverse.network/>.
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
Installation instructions can be found in the installation section of these docs.
## I got an account, now what?
Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register)
At this point you will have two columns in front of you.
### Left column
- first block: here you can see your avatar, your nickname and statistics (Statuses, Following, Followers). Clicking your profile pic will open your profile.
Under that you have a text form which allows you to post new statuses. The number on the bottom of 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.
Under the text form there are also several visibility options and there is the option to use rich text.
Under that the icon on the left is for uploading media files and attach them to your post. There is also an emoji-picker and an option to post a poll.
To post your status, simply press Submit.
On the top right you will also see a wrench icon. This opens your personal settings.
- second block: Here you can switch between the different timelines:
- Timeline: all the people that you follow
- Interactions: here you can switch between different timelines where there was interaction with your account. There is Mentions, Repeats and Favorites, and New follows
- Direct Messages: these are the Direct Messages sent to you
- Public Timeline: all the statutes from the local instance
- The Whole Known Network: all public posts the instance knows about, both local and remote!
- About: This isn't a Timeline but shows relevant info about the instance. You can find a list of the moderators and admins, Terms of Service, MRF policies and enabled features.
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable and by default has links to the pleroma-fe and Mastodon-fe.
- 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
This is where the interesting stuff happens!
Depending on the timeline you will see different statuses, but each status has a standard structure:
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile.
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
- An arrow icon allows you to open the status on the instance where it's originating from.
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.
- Three buttons (left to right): Reply, Repeat, Favorite. There is also a forth button, this is a dropdown menu for simple moderation like muting the conversation or, if you have moderation rights, delete the status from the server.
### Top right
- The magnifier icon opens the search screen where you can search for statuses, people and hashtags. It's also possible to import statusses from remote servers by pasting the url to the post in the search field.
- The gear icon gives you general settings
- If you have admin rights, you'll see an icon that opens the admin interface
- The last icon is to log out
### Bottom right
On the bottom right you have a chatbox. Here you can communicate with people on the same instance in realtime. It is local-only, for now, but there are plans to make it extendable to the entire fediverse!
### 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!
Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.

View File

@ -1,2 +1,2 @@
elixir_version=1.8.2 elixir_version=1.9.4
erlang_version=21.3.7 erlang_version=22.3.4.1

View File

@ -0,0 +1,27 @@
#!/bin/sh
# $FreeBSD$
# PROVIDE: pleroma
# REQUIRE: DAEMON postgresql
# KEYWORD: shutdown
# sudo -u pleroma MIX_ENV=prod elixir --erl \"-detached\" -S mix phx.server
. /etc/rc.subr
name=pleroma
rcvar=pleroma_enable
desc="Pleroma Social Media Platform"
load_rc_config ${name}
: ${pleroma_user:=pleroma}
: ${pleroma_home:=$(getent passwd ${pleroma_user} | awk -F: '{print $6}')}
: ${pleroma_chdir:="${pleroma_home}/pleroma"}
: ${pleroma_env:="HOME=${pleroma_home} MIX_ENV=prod"}
command=/usr/local/bin/elixir
command_args="--erl \"-detached\" -S /usr/local/bin/mix phx.server"
procname="*beam.smp"
run_rc_command "$1"

View File

@ -1,21 +1,45 @@
#!/sbin/openrc-run #!/sbin/openrc-run
supervisor=supervise-daemon
# Requires OpenRC >= 0.35
directory=/opt/pleroma
command=/usr/bin/mix
command_args="phx.server"
command_user=pleroma:pleroma command_user=pleroma:pleroma
command_background=1 command_background=1
export PORT=4000
export MIX_ENV=prod
# Ask process to terminate within 30 seconds, otherwise kill it # Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30/SIGKILL/5" retry="SIGTERM/30/SIGKILL/5"
pidfile="/var/run/pleroma.pid" pidfile="/var/run/pleroma.pid"
directory=/opt/pleroma
healthcheck_delay=60
healthcheck_timer=30
: ${pleroma_port:-4000}
# Needs OpenRC >= 0.42
#respawn_max=0
#respawn_delay=5
# put pleroma_console=YES in /etc/conf.d/pleroma if you want to be able to
# connect to pleroma via an elixir console
if yesno "${pleroma_console}"; then
command=elixir
command_args="--name pleroma@127.0.0.1 --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001 inet_dist_use_interface {127,0,0,1}' -S mix phx.server"
start_post() {
einfo "You can get a console by using this command as pleroma's user:"
einfo "iex --name console@127.0.0.1 --remsh pleroma@127.0.0.1"
}
else
command=/usr/bin/mix
command_args="phx.server"
fi
export MIX_ENV=prod
depend() { depend() {
need nginx postgresql need nginx postgresql
} }
healthcheck() {
# put pleroma_health=YES in /etc/conf.d/pleroma if you want healthchecking
# and make sure you have curl installed
yesno "$pleroma_health" || return 0
curl -q "localhost:${pleroma_port}/api/pleroma/healthcheck"
}

View File

@ -0,0 +1,40 @@
#!/bin/sh
# A simple shell script to delete a media from the Nginx cache.
SCRIPTNAME=${0##*/}
# NGINX cache directory
CACHE_DIRECTORY="/tmp/pleroma-media-cache"
## Return the files where the items are cached.
## $1 - the filename, can be a pattern .
## $2 - the cache directory.
## $3 - (optional) the number of parallel processes to run for grep.
get_cache_files() {
local max_parallel=${3-16}
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E -Rl "^KEY:.*$1" | sort -u
}
## Removes an item from the given cache zone.
## $1 - the filename, can be a pattern .
## $2 - the cache directory.
purge_item() {
for f in $(get_cache_files $1 $2); do
echo "found file: $f"
[ -f $f ] || continue
echo "Deleting $f from $2."
rm $f
done
} # purge_item
purge() {
for url in "$@"
do
echo "$SCRIPTNAME delete \`$url\` from cache ($CACHE_DIRECTORY)"
purge_item $url $CACHE_DIRECTORY
done
}
purge $@

View File

@ -37,18 +37,17 @@ server {
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
ssl_session_timeout 5m; ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# Add TLSv1.0 to support older devices ssl_protocols TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.2;
# Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.)
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers off;
# In case of an old server with an OpenSSL version of 1.0.2 or below, # In case of an old server with an OpenSSL version of 1.0.2 or below,
# leave only prime256v1 or comment out the following line. # leave only prime256v1 or comment out the following line.
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;

View File

@ -3,15 +3,53 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do defmodule Mix.Pleroma do
@apps [
:restarter,
:ecto,
:ecto_sql,
:postgrex,
:db_connection,
:cachex,
:flake_id,
:swoosh,
:timex
]
@cachex_children ["object", "user", "scrubber"]
@doc "Common functions to be reused in mix tasks" @doc "Common functions to be reused in mix tasks"
def start_pleroma do def start_pleroma do
Pleroma.Config.Holder.save_default()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug) Application.put_env(:logger, :console, level: :debug)
end end
{:ok, _} = Application.ensure_all_started(:pleroma) adapter = Application.get_env(:tesla, :adapter)
apps =
if adapter == Tesla.Adapter.Gun do
[:gun | @apps]
else
[:hackney | @apps]
end
Enum.each(apps, &Application.ensure_all_started/1)
children =
[
Pleroma.Repo,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint,
{Oban, Pleroma.Config.get(Oban)}
] ++
http_children(adapter)
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
Supervisor.start_link(children ++ cachex_children,
strategy: :one_for_one,
name: Pleroma.Supervisor
)
if Pleroma.Config.get(:env) not in [:test, :benchmark] do if Pleroma.Config.get(:env) not in [:test, :benchmark] do
pleroma_rebooted?() pleroma_rebooted?()
@ -82,4 +120,11 @@ def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
def escape_sh_path(path) do def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end end
defp http_children(Tesla.Adapter.Gun) do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
defp http_children(_), do: []
end end

View File

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.App do
@moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md")
use Mix.Task
import Mix.Pleroma
@shortdoc "Creates trusted OAuth App"
def run(["create" | options]) do
start_pleroma()
{opts, _} =
OptionParser.parse!(options,
strict: [name: :string, redirect_uri: :string, scopes: :string],
aliases: [n: :name, r: :redirect_uri, s: :scopes]
)
scopes =
if opts[:scopes] do
String.split(opts[:scopes], ",")
else
["read", "write", "follow", "push"]
end
params = %{
client_name: opts[:name],
redirect_uris: opts[:redirect_uri],
trusted: true,
scopes: scopes
}
with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do
shell_info("#{app.client_name} successfully created:")
shell_info("App client_id: " <> app.client_id)
shell_info("App client_secret: " <> app.client_secret)
else
{:error, changeset} ->
shell_error("Creating failed:")
Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} ->
shell_error("#{key}: #{error}")
end)
end
end
end

View File

@ -74,4 +74,43 @@ def run(["render_timeline", nickname | _] = args) do
inputs: inputs inputs: inputs
) )
end end
def run(["adapters"]) do
start_pleroma()
:ok =
Pleroma.Gun.Conn.open(
"https://httpbin.org/stream-bytes/1500",
:gun_connections
)
Process.sleep(1_500)
Benchee.run(
%{
"Without conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [pool: :no_pool, receive_conn: false]
)
end,
"Without conn and with pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [receive_conn: false]
)
end,
"With reused conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [pool: :no_pool]
)
end,
"With reused conn and with pool" => fn ->
{:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
end
},
parallel: 10
)
end
end end

View File

@ -52,6 +52,7 @@ def migrate_to_db(file_path \\ nil) do
defp do_migrate_to_db(config_file) do defp do_migrate_to_db(config_file) do
if File.exists?(config_file) do if File.exists?(config_file) do
shell_info("Migrating settings from file: #{Path.expand(config_file)}")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
@ -72,8 +73,7 @@ defp create(group, settings) do
group group
|> Pleroma.Config.Loader.filter_group(settings) |> Pleroma.Config.Loader.filter_group(settings)
|> Enum.each(fn {key, value} -> |> Enum.each(fn {key, value} ->
key = inspect(key) {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
{:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value})
shell_info("Settings for key #{key} migrated.") shell_info("Settings for key #{key} migrated.")
end) end)
@ -83,7 +83,7 @@ defp create(group, settings) do
defp migrate_from_db(opts) do defp migrate_from_db(opts) do
if Pleroma.Config.get([:configurable_from_database]) do if Pleroma.Config.get([:configurable_from_database]) do
env = opts[:env] || "prod" env = opts[:env] || Pleroma.Config.get(:env)
config_path = config_path =
if Pleroma.Config.get(:release) do if Pleroma.Config.get(:release) do
@ -105,6 +105,10 @@ defp migrate_from_db(opts) do
:ok = File.close(file) :ok = File.close(file)
System.cmd("mix", ["format", config_path]) System.cmd("mix", ["format", config_path])
shell_info(
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
else else
migration_error() migration_error()
end end
@ -112,7 +116,7 @@ defp migrate_from_db(opts) do
defp migration_error do defp migration_error do
shell_error( shell_error(
"Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true." "Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"
) )
end end
@ -131,12 +135,9 @@ defp write_and_delete(config, file, delete?) do
end end
defp write(config, file) do defp write(config, file) do
value = value = inspect(config.value, limit: :infinity)
config.value
|> ConfigDB.from_binary()
|> inspect(limit: :infinity)
IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n") IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
config config
end end

View File

@ -4,11 +4,13 @@
defmodule Mix.Tasks.Pleroma.Database do defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Maintenance
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
import Ecto.Query
import Mix.Pleroma import Mix.Pleroma
use Mix.Task use Mix.Task
@ -34,13 +36,7 @@ def run(["remove_embedded_objects" | args]) do
) )
if Keyword.get(options, :vacuum) do if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL") Maintenance.vacuum("full")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end end
end end
@ -58,8 +54,6 @@ def run(["update_users_following_followers_counts"]) do
end end
def run(["prune_objects" | args]) do def run(["prune_objects" | args]) do
import Ecto.Query
{options, [], []} = {options, [], []} =
OptionParser.parse( OptionParser.parse(
args, args,
@ -94,19 +88,11 @@ def run(["prune_objects" | args]) do
|> Repo.delete_all(timeout: :infinity) |> Repo.delete_all(timeout: :infinity)
if Keyword.get(options, :vacuum) do if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL") Maintenance.vacuum("full")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end end
end end
def run(["fix_likes_collections"]) do def run(["fix_likes_collections"]) do
import Ecto.Query
start_pleroma() start_pleroma()
from(object in Object, from(object in Object,
@ -135,4 +121,39 @@ def run(["fix_likes_collections"]) do
end) end)
|> Stream.run() |> Stream.run()
end end
def run(["vacuum", args]) do
start_pleroma()
Maintenance.vacuum(args)
end
def run(["ensure_expiration"]) do
start_pleroma()
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
Pleroma.Activity
|> join(:left, [a], u in assoc(a, :expiration))
|> join(:inner, [a, _u], o in Object,
on:
fragment(
"(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
o.data,
a.data,
a.data
)
)
|> where(local: true)
|> where([a, u], is_nil(u))
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> where([_a, _u, o], fragment("?->>'type' = 'Note'", o.data))
|> Pleroma.RepoStreamer.chunk_stream(100)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity ->
expires_at = Timex.shift(activity.inserted_at, days: days)
Pleroma.ActivityExpiration.create(activity, expires_at, false)
end)
end)
|> Stream.run()
end
end end

View File

@ -1,5 +1,6 @@
defmodule Mix.Tasks.Pleroma.Digest do defmodule Mix.Tasks.Pleroma.Digest do
use Mix.Task use Mix.Task
import Mix.Pleroma
@shortdoc "Manages digest emails" @shortdoc "Manages digest emails"
@moduledoc File.read!("docs/administration/CLI_tasks/digest.md") @moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
@ -22,12 +23,10 @@ def run(["test", nickname | opts]) do
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
{:ok, _} = Pleroma.Emails.Mailer.deliver(email) {:ok, _} = Pleroma.Emails.Mailer.deliver(email)
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") shell_info("Digest email have been sent to #{nickname} (#{user.email})")
else else
_ -> _ ->
Mix.shell().info( shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
"Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}"
)
end end
end end
end end

View File

@ -41,6 +41,10 @@ def run(args \\ []) do
load_pleroma() load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts = opts =
if opts[:to] || opts[:step] || opts[:all], if opts[:to] || opts[:step] || opts[:all],
do: opts, do: opts,

View File

@ -40,6 +40,10 @@ def run(args \\ []) do
load_pleroma() load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts = opts =
if opts[:to] || opts[:step] || opts[:all], if opts[:to] || opts[:step] || opts[:all],
do: opts, do: opts,

View File

@ -4,18 +4,18 @@
defmodule Mix.Tasks.Pleroma.Emoji do defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task use Mix.Task
import Mix.Pleroma
@shortdoc "Manages emoji packs" @shortdoc "Manages emoji packs"
@moduledoc File.read!("docs/administration/CLI_tasks/emoji.md") @moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")
def run(["ls-packs" | args]) do def run(["ls-packs" | args]) do
Mix.Pleroma.start_pleroma() start_pleroma()
Application.ensure_all_started(:hackney)
{options, [], []} = parse_global_opts(args) {options, [], []} = parse_global_opts(args)
manifest = url_or_path = options[:manifest] || default_manifest()
fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest()) manifest = fetch_and_decode!(url_or_path)
Enum.each(manifest, fn {name, info} -> Enum.each(manifest, fn {name, info} ->
to_print = [ to_print = [
@ -36,19 +36,18 @@ def run(["ls-packs" | args]) do
end end
def run(["get-packs" | args]) do def run(["get-packs" | args]) do
Mix.Pleroma.start_pleroma() start_pleroma()
Application.ensure_all_started(:hackney)
{options, pack_names, []} = parse_global_opts(args) {options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest() url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(manifest_url) manifest = fetch_and_decode!(url_or_path)
for pack_name <- pack_names do for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name] pack = manifest[pack_name]
src_url = pack["src"] src = pack["src"]
IO.puts( IO.puts(
IO.ANSI.format([ IO.ANSI.format([
@ -58,11 +57,11 @@ def run(["get-packs" | args]) do
:normal, :normal,
" from ", " from ",
:underline, :underline,
src_url src
]) ])
) )
binary_archive = Tesla.get!(client(), src_url).body {:ok, binary_archive} = fetch(src)
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
@ -75,8 +74,11 @@ def run(["get-packs" | args]) do
raise "Bad SHA256 for #{pack_name}" raise "Bad SHA256 for #{pack_name}"
end end
# The url specified in files should be in the same directory # The location specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"]) files_loc =
url_or_path
|> Path.dirname()
|> Path.join(pack["files"])
IO.puts( IO.puts(
IO.ANSI.format([ IO.ANSI.format([
@ -86,11 +88,11 @@ def run(["get-packs" | args]) do
:normal, :normal,
" from ", " from ",
:underline, :underline,
files_url files_loc
]) ])
) )
files = Tesla.get!(client(), files_url).body |> Jason.decode!() files = fetch_and_decode!(files_loc)
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@ -134,38 +136,51 @@ def run(["get-packs" | args]) do
end end
end end
def run(["gen-pack", src]) do def run(["gen-pack" | args]) do
Application.ensure_all_started(:hackney) start_pleroma()
proposed_name = Path.basename(src) |> Path.rootname() {opts, [src], []} =
name = String.trim(IO.gets("Pack name [#{proposed_name}]: ")) OptionParser.parse(
# If there's no name, use the default one args,
name = if String.length(name) > 0, do: name, else: proposed_name strict: [
name: :string,
license = String.trim(IO.gets("License: ")) license: :string,
homepage = String.trim(IO.gets("Homepage: ")) homepage: :string,
description = String.trim(IO.gets("Description: ")) description: :string,
files: :string,
proposed_files_name = "#{name}.json" extensions: :string
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}]: ")
) )
proposed_name = Path.basename(src) |> Path.rootname()
name = get_option(opts, :name, "Pack name:", proposed_name)
license = get_option(opts, :license, "License:")
homepage = get_option(opts, :homepage, "Homepage:")
description = get_option(opts, :description, "Description:")
proposed_files_name = "#{name}_files.json"
files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)
default_exts = [".png", ".gif"]
custom_exts =
get_option(
opts,
:extensions,
"Emoji file extensions (separated with spaces):",
Enum.join(default_exts, " ")
)
|> String.split(" ", trim: true)
exts = exts =
if String.length(exts) > 0 do if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts default_exts
else
custom_exts
end end
IO.puts("Using #{Enum.join(exts, " ")} extensions")
IO.puts("Downloading the pack and generating SHA256") IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(client(), src).body binary_archive = Tesla.get!(client(), src).body
@ -195,14 +210,16 @@ def run(["gen-pack", src]) do
IO.puts(""" IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack. #{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. Please review the files in the pack and remove those not needed.
""") """)
if File.exists?("index.json") do pack_file = "#{name}.json"
existing_data = File.read!("index.json") |> Jason.decode!()
if File.exists?(pack_file) do
existing_data = File.read!(pack_file) |> Jason.decode!()
File.write!( File.write!(
"index.json", pack_file,
Jason.encode!( Jason.encode!(
Map.merge( Map.merge(
existing_data, existing_data,
@ -212,24 +229,36 @@ def run(["gen-pack", src]) do
) )
) )
IO.puts("index.json file has been update with the #{name} pack") IO.puts("#{pack_file} has been updated with the #{name} pack")
else else
File.write!("index.json", Jason.encode!(pack_json, pretty: true)) File.write!(pack_file, Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack") IO.puts("#{pack_file} has been created with the #{name} pack")
end end
end end
defp fetch_manifest(from) do def run(["reload"]) do
Jason.decode!( start_pleroma()
if String.starts_with?(from, "http") do Pleroma.Emoji.reload()
Tesla.get!(client(), from).body IO.puts("Emoji packs have been reloaded.")
end
defp fetch_and_decode!(from) do
with {:ok, json} <- fetch(from) do
Jason.decode!(json)
else else
File.read!(from) {:error, error} -> raise "#{from} cannot be fetched. Error: #{error} occur."
end end
)
end end
defp fetch("http" <> _ = from) do
with {:ok, %{body: body}} <- Tesla.get(client(), from) do
{:ok, body}
end
end
defp fetch(path), do: File.read(path)
defp parse_global_opts(args) do defp parse_global_opts(args) do
OptionParser.parse( OptionParser.parse(
args, args,

View File

@ -0,0 +1,140 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Frontend do
use Mix.Task
import Mix.Pleroma
@shortdoc "Manages bundled Pleroma frontends"
@moduledoc File.read!("docs/administration/CLI_tasks/frontend.md")
def run(["install", "none" | _args]) do
shell_info("Skipping frontend installation because none was requested")
"none"
end
def run(["install", frontend | args]) do
log_level = Logger.level()
Logger.configure(level: :warn)
start_pleroma()
{options, [], []} =
OptionParser.parse(
args,
strict: [
ref: :string,
static_dir: :string,
build_url: :string,
build_dir: :string,
file: :string
]
)
instance_static_dir =
with nil <- options[:static_dir] do
Pleroma.Config.get!([:instance, :static_dir])
end
cmd_frontend_info = %{
"name" => frontend,
"ref" => options[:ref],
"build_url" => options[:build_url],
"build_dir" => options[:build_dir]
}
config_frontend_info = Pleroma.Config.get([:frontends, :available, frontend], %{})
frontend_info =
Map.merge(config_frontend_info, cmd_frontend_info, fn _key, config, cmd ->
# This only overrides things that are actually set
cmd || config
end)
ref = frontend_info["ref"]
unless ref do
raise "No ref given or configured"
end
dest =
Path.join([
instance_static_dir,
"frontends",
frontend,
ref
])
fe_label = "#{frontend} (#{ref})"
tmp_dir = Path.join(dest, "tmp")
with {_, :ok} <-
{:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])},
shell_info("Installing #{fe_label} to #{dest}"),
:ok <- install_frontend(frontend_info, tmp_dir, dest) do
File.rm_rf!(tmp_dir)
shell_info("Frontend #{fe_label} installed to #{dest}")
Logger.configure(level: log_level)
else
{:download_or_unzip, _} ->
shell_info("Could not download or unzip the frontend")
_e ->
shell_info("Could not install the frontend")
end
end
defp download_or_unzip(frontend_info, temp_dir, file) do
if file do
with {:ok, zip} <- File.read(Path.expand(file)) do
unzip(zip, temp_dir)
end
else
download_build(frontend_info, temp_dir)
end
end
def unzip(zip, dest) do
with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
File.rm_rf!(dest)
File.mkdir_p!(dest)
Enum.each(unzipped, fn {filename, data} ->
path = filename
new_file_path = Path.join(dest, path)
new_file_path
|> Path.dirname()
|> File.mkdir_p!()
File.write!(new_file_path, data)
end)
:ok
end
end
defp download_build(frontend_info, dest) do
shell_info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], timeout: 120_000, recv_timeout: 120_000) do
unzip(zip_body, dest)
else
e -> {:error, e}
end
end
defp install_frontend(frontend_info, source, dest) do
from = frontend_info["build_dir"] || "dist"
File.mkdir_p!(dest)
File.cp_r!(Path.join([source, from]), dest)
:ok
end
end

View File

@ -145,16 +145,18 @@ def run(["gen" | rest]) do
options, options,
:uploads_dir, :uploads_dir,
"What directory should media uploads go in (when using the local uploader)?", "What directory should media uploads go in (when using the local uploader)?",
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) Config.get([Pleroma.Uploaders.Local, :uploads])
) )
|> Path.expand()
static_dir = static_dir =
get_option( get_option(
options, options,
:static_dir, :static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Pleroma.Config.get([:instance, :static_dir]) Config.get([:instance, :static_dir])
) )
|> Path.expand()
Config.put([:instance, :static_dir], static_dir) Config.put([:instance, :static_dir], static_dir)
@ -204,7 +206,7 @@ def run(["gen" | rest]) do
shell_info("Writing the postgres script to #{psql_path}.") shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql) File.write(psql_path, result_psql)
write_robots_txt(indexable, template_dir) write_robots_txt(static_dir, indexable, template_dir)
shell_info( shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps." "\n All files successfully written! Refer to the installation instructions for your platform for next steps."
@ -224,15 +226,13 @@ def run(["gen" | rest]) do
end end
end end
defp write_robots_txt(indexable, template_dir) do defp write_robots_txt(static_dir, indexable, template_dir) do
robots_txt = robots_txt =
EEx.eval_file( EEx.eval_file(
template_dir <> "/robots_txt.eex", template_dir <> "/robots_txt.eex",
indexable: indexable indexable: indexable
) )
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
unless File.exists?(static_dir) do unless File.exists?(static_dir) do
File.mkdir_p!(static_dir) File.mkdir_p!(static_dir)
end end

View File

@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
@moduledoc """ @moduledoc """
Example: Example:
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user > mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588" # set false only for parallel588 user
> mix pleroma.notification_settings --privacy-option=true # set true for all users > mix pleroma.notification_settings --hide-notification-contents=true # set true for all users
""" """
@ -19,16 +19,16 @@ def run(args) do
OptionParser.parse( OptionParser.parse(
args, args,
strict: [ strict: [
privacy_option: :boolean, hide_notification_contents: :boolean,
email_users: :string, email_users: :string,
nickname_users: :string nickname_users: :string
] ]
) )
privacy_option = Keyword.get(options, :privacy_option) hide_notification_contents = Keyword.get(options, :hide_notification_contents)
if not is_nil(privacy_option) do if not is_nil(hide_notification_contents) do
privacy_option hide_notification_contents
|> build_query(options) |> build_query(options)
|> Pleroma.Repo.update_all([]) |> Pleroma.Repo.update_all([])
end end
@ -36,15 +36,15 @@ def run(args) do
shell_info("Done") shell_info("Done")
end end
defp build_query(privacy_option, options) do defp build_query(hide_notification_contents, options) do
query = query =
from(u in Pleroma.User, from(u in Pleroma.User,
update: [ update: [
set: [ set: [
notification_settings: notification_settings:
fragment( fragment(
"jsonb_set(notification_settings, '{privacy_option}', ?)", "jsonb_set(notification_settings, '{hide_notification_contents}', ?)",
^privacy_option ^hide_notification_contents
) )
] ]
] ]

View File

@ -17,30 +17,53 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
def run([]) do def run([]) do
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()
["public", "unlisted", "private", "direct"] instances =
|> Enum.each(fn visibility -> Activity
count = status_visibility_count_query(visibility) |> distinct([a], true)
name = "status_visibility_#{visibility}" |> select([a], fragment("split_part(?, '/', 3)", a.actor))
CounterCache.set(name, count) |> Repo.all()
Mix.Pleroma.shell_info("Set #{name} to #{count}")
instances
|> Enum.with_index(1)
|> Enum.each(fn {instance, i} ->
counters = instance_counters(instance)
CounterCache.set(instance, counters)
Mix.Pleroma.shell_info(
"[#{i}/#{length(instances)}] Setting #{instance} counters: #{inspect(counters)}"
)
end) end)
Mix.Pleroma.shell_info("Done") Mix.Pleroma.shell_info("Done")
end end
defp status_visibility_count_query(visibility) do defp instance_counters(instance) do
counters = %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
Activity Activity
|> where( |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> where([a], fragment("split_part(?, '/', 3) = ?", a.actor, ^instance))
|> select(
[a], [a],
fragment( {fragment(
"activity_visibility(?, ?, ?) = ?", "activity_visibility(?, ?, ?)",
a.actor, a.actor,
a.recipients, a.recipients,
a.data, a.data
^visibility ), count(a.id)}
)
|> group_by(
[a],
fragment(
"activity_visibility(?, ?, ?)",
a.actor,
a.recipients,
a.data
) )
) )
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) |> Repo.all(timeout: :timer.minutes(30))
|> Repo.aggregate(:count, :id, timeout: :timer.minutes(30)) |> Enum.reduce(counters, fn {visibility, count}, acc ->
Map.put(acc, visibility, count)
end)
end end
end end

View File

@ -35,10 +35,16 @@ def run(["unfollow", target]) do
def run(["list"]) do def run(["list"]) do
start_pleroma() start_pleroma()
with {:ok, list} <- Relay.list(true) do with {:ok, list} <- Relay.list() do
list |> Enum.each(&shell_info(&1)) Enum.each(list, &print_relay_url/1)
else else
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end end
end end
defp print_relay_url(%{followed_back: false} = relay) do
shell_info("#{relay.actor} - no Accept received (relay didn't follow back)")
end
defp print_relay_url(relay), do: shell_info(relay.actor)
end end

View File

@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md") @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -96,8 +98,9 @@ def run(["new", nickname, email | rest]) do
def run(["rm", nickname]) do def run(["rm", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
User.perform(:delete, user) {:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.") shell_info("User #{nickname} deleted.")
else else
_ -> shell_error("No local user #{nickname}") _ -> shell_error("No local user #{nickname}")
@ -141,28 +144,30 @@ def run(["reset_password", nickname]) do
end end
end end
def run(["unsubscribe", nickname]) do def run(["reset_mfa", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, _token} <- Pleroma.MFA.disable(user) do
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["deactivate", nickname]) do
start_pleroma() start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}") shell_info("Deactivating #{user.nickname}")
User.deactivate(user) User.deactivate(user)
user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
:timer.sleep(500) :timer.sleep(500)
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
if Enum.empty?(User.get_friends(user)) do if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}") shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
end end
else else
_ -> _ ->
@ -170,7 +175,7 @@ def run(["unsubscribe", nickname]) do
end end
end end
def run(["unsubscribe_all_from_instance", instance]) do def run(["deactivate_all_from_instance", instance]) do
start_pleroma() start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"}) Pleroma.User.Query.build(%{nickname: "@#{instance}"})
@ -178,7 +183,7 @@ def run(["unsubscribe_all_from_instance", instance]) do
|> Stream.each(fn users -> |> Stream.each(fn users ->
users users
|> Enum.each(fn user -> |> Enum.each(fn user ->
run(["unsubscribe", user.nickname]) run(["deactivate", user.nickname])
end) end)
end) end)
|> Stream.run() |> Stream.run()
@ -227,7 +232,7 @@ def run(["tag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags) user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") shell_error("Could not change user tags for #{nickname}")
@ -240,7 +245,7 @@ def run(["untag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags) user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") shell_error("Could not change user tags for #{nickname}")

View File

@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
"Create" => "mention",
"Follow" => ["follow", "follow_request"],
"Announce" => "reblog",
"Like" => "favourite",
"Move" => "move",
"EmojiReact" => "pleroma:emoji_reaction"
}
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -41,6 +31,10 @@ defmodule Pleroma.Activity do
field(:recipients, {:array, :string}, default: []) field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true) field(:thread_muted?, :boolean, virtual: true)
# A field that can be used if you need to join some kind of other
# id to order / paginate this field by
field(:pagination_id, :string, virtual: true)
# This is a fake relation, # This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor # do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
@ -300,32 +294,6 @@ def follow_accepted?(
def follow_accepted?(_), do: false def follow_accepted?(_), do: false
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type)
end
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
if follow_accepted?(activity) do
"follow"
else
"follow_request"
end
end
def mastodon_notification_type(%Activity{}), do: nil
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
@doc "Converts Mastodon notification type to AR activity type"
def from_mastodon_notification_type(type) do
with {k, _v} <-
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
k
end
end
def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: [] def all_by_actor_and_id(_actor, []), do: []
@ -372,4 +340,10 @@ def direct_conversation_id(activity, for_user) do
_ -> nil _ -> nil
end end
end end
@spec pinned_by_actor?(Activity.t()) :: boolean()
def pinned_by_actor?(%Activity{} = activity) do
actor = user_actor(activity)
activity.id in actor.pinned_activities
end
end end

View File

@ -32,6 +32,13 @@ def by_author(query \\ Activity, %User{ap_id: ap_id}) do
from(a in query, where: a.actor == ^ap_id) from(a in query, where: a.actor == ^ap_id)
end end
def find_by_object_ap_id(activities, object_ap_id) do
Enum.find(
activities,
&(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]])
)
end
@spec by_object_id(query, String.t() | [String.t()]) :: query @spec by_object_id(query, String.t() | [String.t()]) :: query
def by_object_id(query \\ Activity, object_id) def by_object_id(query \\ Activity, object_id)

View File

@ -20,11 +20,11 @@ defmodule Pleroma.ActivityExpiration do
field(:scheduled_at, :naive_datetime) field(:scheduled_at, :naive_datetime)
end end
def changeset(%ActivityExpiration{} = expiration, attrs) do def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do
expiration expiration
|> cast(attrs, [:scheduled_at]) |> cast(attrs, [:scheduled_at])
|> validate_required([:scheduled_at]) |> validate_required([:scheduled_at])
|> validate_scheduled_at() |> validate_scheduled_at(validate_scheduled_at)
end end
def get_by_activity_id(activity_id) do def get_by_activity_id(activity_id) do
@ -33,9 +33,9 @@ def get_by_activity_id(activity_id) do
|> Repo.one() |> Repo.one()
end end
def create(%Activity{} = activity, scheduled_at) do def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do
%ActivityExpiration{activity_id: activity.id} %ActivityExpiration{activity_id: activity.id}
|> changeset(%{scheduled_at: scheduled_at}) |> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at)
|> Repo.insert() |> Repo.insert()
end end
@ -46,10 +46,17 @@ def due_expirations(offset \\ 0) do
ActivityExpiration ActivityExpiration
|> where([exp], exp.scheduled_at < ^naive_datetime) |> where([exp], exp.scheduled_at < ^naive_datetime)
|> limit(50)
|> preload(:activity)
|> Repo.all() |> Repo.all()
|> Enum.reject(fn %{activity: activity} ->
Activity.pinned_by_actor?(activity)
end)
end end
def validate_scheduled_at(changeset) do def validate_scheduled_at(changeset, false), do: changeset
def validate_scheduled_at(changeset, true) do
validate_change(changeset, :scheduled_at, fn _, scheduled_at -> validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
if not expires_late_enough?(scheduled_at) do if not expires_late_enough?(scheduled_at) do
[scheduled_at: "an ephemeral activity must live for at least one hour"] [scheduled_at: "an ephemeral activity must live for at least one hour"]

View File

@ -3,8 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do defmodule Pleroma.Application do
import Cachex.Spec
use Application use Application
import Cachex.Spec
alias Pleroma.Config
require Logger require Logger
@name Mix.Project.config()[:name] @name Mix.Project.config()[:name]
@ -18,9 +22,9 @@ def named_version, do: @name <> " " <> @version
def repository, do: @repository def repository, do: @repository
def user_agent do def user_agent do
case Pleroma.Config.get([:http, :user_agent], :default) do case Config.get([:http, :user_agent], :default) do
:default -> :default ->
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>" info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info named_version() <> "; " <> info
custom -> custom ->
@ -31,29 +35,59 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
Pleroma.Config.Holder.save_default() # Scrubbers are compiled at runtime and therefore will cause a conflict
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers() Pleroma.HTML.compile_scrubbers()
Pleroma.Config.DeprecationWarnings.warn() Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.Repo.check_migrations_applied!() Pleroma.ApplicationRequirements.verify!()
setup_instrumenters() setup_instrumenters()
load_custom_modules() load_custom_modules()
check_system_commands()
Pleroma.Docs.JSON.compile()
adapter = Application.get_env(:tesla, :adapter)
if adapter == Tesla.Adapter.Gun do
if version = Pleroma.OTPVersion.version() do
[major, minor] =
version
|> String.split(".")
|> Enum.map(&String.to_integer/1)
|> Enum.take(2)
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!OTP VERSION WARNING!!!
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
"
end
else
raise "
!!!OTP VERSION WARNING!!!
To support correct handling of unordered certificates chains - OTP version must be > 22.2.
"
end
end
# Define workers and child supervisors to be supervised # Define workers and child supervisors to be supervised
children = children =
[ [
Pleroma.Repo, Pleroma.Repo,
Pleroma.Config.TransferTask, Config.TransferTask,
Pleroma.Emoji, Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.Plugs.RateLimiter.Supervisor Pleroma.Plugs.RateLimiter.Supervisor
] ++ ] ++
cachex_children() ++ cachex_children() ++
hackney_pool_children() ++ http_children(adapter, @env) ++
[ [
Pleroma.Stats, Pleroma.Stats,
Pleroma.JobQueueMonitor, Pleroma.JobQueueMonitor,
{Oban, Pleroma.Config.get(Oban)} {Oban, Config.get(Oban)}
] ++ ] ++
task_children(@env) ++ task_children(@env) ++
streamer_child(@env) ++ streamer_child(@env) ++
@ -70,7 +104,7 @@ def start(_type, _args) do
end end
def load_custom_modules do def load_custom_modules do
dir = Pleroma.Config.get([:modules, :runtime_dir]) dir = Config.get([:modules, :runtime_dir])
if dir && File.exists?(dir) do if dir && File.exists?(dir) do
dir dir
@ -111,20 +145,6 @@ defp setup_instrumenters do
Pleroma.Web.Endpoint.Instrumenter.setup() Pleroma.Web.Endpoint.Instrumenter.setup()
end end
def enabled_hackney_pools do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
[:federation]
else
[]
end ++
if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
[:upload]
else
[]
end
end
defp cachex_children do defp cachex_children do
[ [
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()), build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
@ -135,7 +155,8 @@ defp cachex_children do
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500), build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500), build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500) build_cachex("failed_proxy_url", limit: 2500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
] ]
end end
@ -146,21 +167,29 @@ defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60)) do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
defp seconds_valid_interval, defp seconds_valid_interval,
do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])) do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
defp build_cachex(type, opts), @spec build_cachex(String.t(), keyword()) :: map()
def build_cachex(type, opts),
do: %{ do: %{
id: String.to_atom("cachex_" <> type), id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
type: :worker type: :worker
} }
defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled]) defp chat_enabled?, do: Config.get([:chat, :enabled])
defp streamer_child(:test), do: [] defp streamer_child(env) when env in [:test, :benchmark], do: []
defp streamer_child(_) do defp streamer_child(_) do
[Pleroma.Web.Streamer.supervisor()] [
{Registry,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
]}
]
end end
defp chat_child(_env, true) do defp chat_child(_env, true) do
@ -169,13 +198,6 @@ defp chat_child(_env, true) do
defp chat_child(_, _), do: [] defp chat_child(_, _), do: []
defp hackney_pool_children do
for pool <- enabled_hackney_pools() do
options = Pleroma.Config.get([:hackney_pools, pool])
:hackney_pool.child_spec(pool, options)
end
end
defp task_children(:test) do defp task_children(:test) do
[ [
%{ %{
@ -200,4 +222,49 @@ defp task_children(_) do
} }
] ]
end end
# start hackney and gun pools in tests
defp http_children(_, :test) do
http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
end
defp http_children(Tesla.Adapter.Hackney, _) do
pools = [:federation, :media]
pools =
if Config.get([Pleroma.Upload, :proxy_remote]) do
[:upload | pools]
else
pools
end
for pool <- pools do
options = Config.get([:hackney_pools, pool])
:hackney_pool.child_spec(pool, options)
end
end
defp http_children(Tesla.Adapter.Gun, _) do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
defp http_children(_, _), do: []
defp check_system_commands do
filters = Config.get([Pleroma.Upload, :filters])
check_filter = fn filter, command_required ->
with true <- filter in filters,
false <- Pleroma.Utils.command_available?(command_required) do
Logger.error(
"#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found"
)
end
end
check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool")
check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify")
check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify")
end
end end

View File

@ -0,0 +1,143 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ApplicationRequirements do
@moduledoc """
The module represents the collection of validations to runs before start server.
"""
defmodule VerifyError, do: defexception([:message])
import Ecto.Query
require Logger
@spec verify!() :: :ok | VerifyError.t()
def verify! do
:ok
|> check_confirmation_accounts!
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
|> handle_result()
end
defp handle_result(:ok), do: :ok
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do
Logger.error("""
To send welcome email do you need to enable mail.
\nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true
""")
{:error, "The mail disabled."}
else
:ok
end
end
defp check_welcome_message_config!(result), do: result
# Checks account confirmation email
#
def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
Logger.error(
"Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
)
{:error,
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
else
:ok
end
end
def check_confirmation_accounts!(result), do: result
# Checks for pending migrations.
#
def check_migrations_applied!(:ok) do
unless Pleroma.Config.get(
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
false
) do
{_, res, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
down_migrations =
Ecto.Migrator.migrations(repo)
|> Enum.reject(fn
{:up, _, _} -> true
{:down, _, _} -> false
end)
if length(down_migrations) > 0 do
down_migrations_text =
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
Logger.error(
"The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
)
{:error, "Unapplied Migrations detected"}
else
:ok
end
end)
res
else
:ok
end
end
def check_migrations_applied!(result), do: result
# Checks for settings of RUM indexes.
#
defp check_rum!(:ok) do
{_, res, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
migrate =
from(o in "columns",
where: o.table_name == "objects",
where: o.column_name == "fts_content"
)
|> repo.exists?(prefix: "information_schema")
setting = Pleroma.Config.get([:database, :rum_enabled], false)
do_check_rum!(setting, migrate)
end)
res
end
defp check_rum!(result), do: result
defp do_check_rum!(setting, migrate) do
case {setting, migrate} do
{true, false} ->
Logger.error(
"Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
)
{:error, "Unapplied RUM Migrations detected"}
{false, true} ->
Logger.error(
"Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
)
{:error, "RUM Migrations detected"}
_ ->
:ok
end
end
end

View File

@ -4,7 +4,7 @@
defmodule Pleroma.BBS.Authenticator do defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator use Sshd.PasswordAuthenticator
alias Comeonin.Pbkdf2 alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User alias Pleroma.User
def authenticate(username, password) do def authenticate(username, password) do
@ -12,7 +12,7 @@ def authenticate(username, password) do
password = to_string(password) password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do with %User{} = user <- User.get_by_nickname(username) do
Pbkdf2.checkpw(password, user.password_hash) AuthenticationPlug.checkpw(password, user.password_hash)
else else
_e -> false _e -> false
end end

View File

@ -66,7 +66,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
with %Activity{} <- Activity.get_by_id(activity_id), with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <- {:ok, _activity} <-
CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!") IO.puts("Replied!")
else else
_e -> IO.puts("Could not reply...") _e -> IO.puts("Could not reply...")
@ -78,7 +78,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
def handle_command(%{user: user} = state, "p " <> text) do def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text) text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!") IO.puts("Posted!")
else else
_e -> IO.puts("Could not post...") _e -> IO.puts("Could not post...")
@ -92,10 +92,10 @@ def handle_command(state, "home") do
params = params =
%{} %{}
|> Map.put("type", ["Create"]) |> Map.put(:type, ["Create"])
|> Map.put("blocking_user", user) |> Map.put(:blocking_user, user)
|> Map.put("muting_user", user) |> Map.put(:muting_user, user)
|> Map.put("user", user) |> Map.put(:user, user)
activities = activities =
[user.ap_id | Pleroma.User.following(user)] [user.ap_id | Pleroma.User.following(user)]

View File

@ -3,53 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do defmodule Pleroma.Captcha do
import Pleroma.Web.Gettext
alias Calendar.DateTime alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor alias Plug.Crypto.MessageEncryptor
use GenServer
@doc false
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc false
def init(_) do
{:ok, nil}
end
@doc """ @doc """
Ask the configured captcha service for a new captcha Ask the configured captcha service for a new captcha
""" """
def new do def new do
GenServer.call(__MODULE__, :new) if not enabled?() do
end %{type: :none}
@doc """
Ask the configured captcha service to validate the captcha
"""
def validate(token, captcha, answer_data) do
GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
end
@doc false
def handle_call(:new, _from, state) do
enabled = Pleroma.Config.get([__MODULE__, :enabled])
if !enabled do
{:reply, %{type: :none}, state}
else else
new_captcha = method().new() new_captcha = method().new()
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
# This make salt a little different for two keys # This make salt a little different for two keys
token = new_captcha[:token] {secret, sign_secret} = secret_pair(new_captcha[:token])
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# Basically copy what Phoenix.Token does here, add the time to # Basically copy what Phoenix.Token does here, add the time to
# the actual data and make it a binary to then encrypt it # the actual data and make it a binary to then encrypt it
encrypted_captcha_answer = encrypted_captcha_answer =
@ -60,55 +29,73 @@ def handle_call(:new, _from, state) do
|> :erlang.term_to_binary() |> :erlang.term_to_binary()
|> MessageEncryptor.encrypt(secret, sign_secret) |> MessageEncryptor.encrypt(secret, sign_secret)
{
:reply,
# Replace the answer with the encrypted answer # Replace the answer with the encrypted answer
%{new_captcha | answer_data: encrypted_captcha_answer}, %{new_captcha | answer_data: encrypted_captcha_answer}
state
}
end end
end end
@doc false @doc """
def handle_call({:validate, token, captcha, answer_data}, _from, state) do Ask the configured captcha service to validate the captcha
"""
def validate(token, captcha, answer_data) do
with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data),
:ok <- validate_expiration(at),
:ok <- validate_usage(token),
:ok <- method().validate(token, captcha, answer_md5),
{:ok, _} <- mark_captcha_as_used(token) do
:ok
end
end
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false)
defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid])
defp secret_pair(token) do
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# If the time found is less than (current_time-seconds_valid) then the time has already passed {secret, sign_secret}
# Later we check that the time found is more than the presumed invalidatation time, that means end
# that the data is still valid and the captcha can be checked
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) defp validate_answer_data(token, answer_data) do
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid) {secret, sign_secret} = secret_pair(token)
result =
with false <- is_nil(answer_data), with false <- is_nil(answer_data),
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
try do {:ok, %{at: at, answer_data: answer_md5}}
if DateTime.before?(at, valid_if_after),
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
res = method().validate(token, captcha, answer_md5)
# Throw if an error occurs
if res != :ok, do: throw(res)
# Mark this captcha as used
{:ok, _} =
Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid))
:ok
catch
:throw, e -> e
end
else else
_ -> {:error, dgettext("errors", "Invalid answer data")} _ -> {:error, :invalid_answer_data}
end
end end
{:reply, result, state} defp validate_expiration(created_at) do
# If the time found is less than (current_time-seconds_valid) then the time has already passed
# Later we check that the time found is more than the presumed invalidatation time, that means
# that the data is still valid and the captcha can be checked
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
if DateTime.before?(created_at, valid_if_after) do
{:error, :expired}
else
:ok
end
end
defp validate_usage(token) do
if is_nil(Cachex.get!(:used_captcha_cache, token)) do
:ok
else
{:error, :already_used}
end
end
defp mark_captcha_as_used(token) do
ttl = seconds_valid() |> :timer.seconds()
Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
end end
defp method, do: Pleroma.Config.get!([__MODULE__, :method]) defp method, do: Pleroma.Config.get!([__MODULE__, :method])

View File

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do defmodule Pleroma.Captcha.Kocaptcha do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service alias Pleroma.Captcha.Service
@behaviour Service @behaviour Service
@ -13,7 +12,7 @@ def new do
case Tesla.get(endpoint <> "/new") do case Tesla.get(endpoint <> "/new") do
{:error, _} -> {:error, _} ->
%{error: dgettext("errors", "Kocaptcha service unavailable")} %{error: :kocaptcha_service_unavailable}
{:ok, res} -> {:ok, res} ->
json_resp = Jason.decode!(res.body) json_resp = Jason.decode!(res.body)
@ -22,7 +21,8 @@ def new do
type: :kocaptcha, type: :kocaptcha,
token: json_resp["token"], token: json_resp["token"],
url: endpoint <> json_resp["url"], url: endpoint <> json_resp["url"],
answer_data: json_resp["md5"] answer_data: json_resp["md5"],
seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
} }
end end
end end
@ -33,6 +33,6 @@ def validate(_token, captcha, answer_data) do
if not is_nil(captcha) and if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data), :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok, do: :ok,
else: {:error, dgettext("errors", "Invalid CAPTCHA")} else: {:error, :invalid}
end end
end end

View File

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do defmodule Pleroma.Captcha.Native do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service alias Pleroma.Captcha.Service
@behaviour Service @behaviour Service
@ -11,21 +10,22 @@ defmodule Pleroma.Captcha.Native do
def new do def new do
case Captcha.get() do case Captcha.get() do
:error -> :error ->
%{error: dgettext("errors", "Captcha error")} %{error: :captcha_error}
{:ok, answer_data, img_binary} -> {:ok, answer_data, img_binary} ->
%{ %{
type: :native, type: :native,
token: token(), token: token(),
url: "data:image/png;base64," <> Base.encode64(img_binary), url: "data:image/png;base64," <> Base.encode64(img_binary),
answer_data: answer_data answer_data: answer_data,
seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
} }
end end
end end
@impl Service @impl Service
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")} def validate(_token, _captcha, _answer), do: {:error, :invalid}
defp token do defp token do
10 10

72
lib/pleroma/chat.ex Normal file
View File

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
@moduledoc """
Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
"""
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "chats" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:recipient, :string)
timestamps()
end
def changeset(struct, params) do
struct
|> cast(params, [:user_id, :recipient])
|> validate_change(:recipient, fn
:recipient, recipient ->
case User.get_cached_by_ap_id(recipient) do
nil -> [recipient: "must be an existing user"]
_ -> []
end
end)
|> validate_required([:user_id, :recipient])
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
end
def get_by_id(id) do
__MODULE__
|> Repo.get(id)
end
def get(user_id, recipient) do
__MODULE__
|> Repo.get_by(user_id: user_id, recipient: recipient)
end
def get_or_create(user_id, recipient) do
%__MODULE__{}
|> changeset(%{user_id: user_id, recipient: recipient})
|> Repo.insert(
# Need to set something, otherwise we get nothing back at all
on_conflict: [set: [recipient: recipient]],
returning: true,
conflict_target: [:user_id, :recipient]
)
end
def bump_or_create(user_id, recipient) do
%__MODULE__{}
|> changeset(%{user_id: user_id, recipient: recipient})
|> Repo.insert(
on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: [:user_id, :recipient]
)
end
end

View File

@ -0,0 +1,117 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat.MessageReference do
@moduledoc """
A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
by them, or should be displayed to them. Used to build the chat view that is presented to the user.
"""
use Ecto.Schema
alias Pleroma.Chat
alias Pleroma.Object
alias Pleroma.Repo
import Ecto.Changeset
import Ecto.Query
@primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
schema "chat_message_references" do
belongs_to(:object, Object)
belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType)
field(:unread, :boolean, default: true)
timestamps()
end
def changeset(struct, params) do
struct
|> cast(params, [:object_id, :chat_id, :unread])
|> validate_required([:object_id, :chat_id, :unread])
end
def get_by_id(id) do
__MODULE__
|> Repo.get(id)
|> Repo.preload(:object)
end
def delete(cm_ref) do
cm_ref
|> Repo.delete()
end
def delete_for_object(%{id: object_id}) do
from(cr in __MODULE__,
where: cr.object_id == ^object_id
)
|> Repo.delete_all()
end
def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
__MODULE__
|> Repo.get_by(chat_id: chat_id, object_id: object_id)
|> Repo.preload(:object)
end
def for_chat_query(chat) do
from(cr in __MODULE__,
where: cr.chat_id == ^chat.id,
order_by: [desc: :id],
preload: [:object]
)
end
def last_message_for_chat(chat) do
chat
|> for_chat_query()
|> limit(1)
|> Repo.one()
end
def create(chat, object, unread) do
params = %{
chat_id: chat.id,
object_id: object.id,
unread: unread
}
%__MODULE__{}
|> changeset(params)
|> Repo.insert()
end
def unread_count_for_chat(chat) do
chat
|> for_chat_query()
|> where([cmr], cmr.unread == true)
|> Repo.aggregate(:count)
end
def mark_as_read(cm_ref) do
cm_ref
|> changeset(%{unread: false})
|> Repo.update()
end
def set_all_seen_for_chat(chat, last_read_id \\ nil) do
query =
chat
|> for_chat_query()
|> exclude(:order_by)
|> exclude(:preload)
|> where([cmr], cmr.unread == true)
if last_read_id do
query
|> where([cmr], cmr.id <= ^last_read_id)
else
query
end
|> Repo.update_all(set: [unread: false])
end
end

View File

@ -11,12 +11,10 @@ def get(key), do: get(key, nil)
def get([key], default), do: get(key, default) def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do def get([_ | _] = path, default) do
case :pleroma case fetch(path) do
|> Application.get_env(parent_key) {:ok, value} -> value
|> get_in(keys) do :error -> default
nil -> default
any -> any
end end
end end
@ -34,6 +32,24 @@ def get!(key) do
end end
end end
def fetch(key) when is_atom(key), do: fetch([key])
def fetch([root_key | keys]) do
Enum.reduce_while(keys, Application.fetch_env(:pleroma, root_key), fn
key, {:ok, config} when is_map(config) or is_list(config) ->
case Access.fetch(config, key) do
:error ->
{:halt, :error}
value ->
{:cont, value}
end
_key, _config ->
{:halt, :error}
end)
end
def put([key], value), do: put(key, value) def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do def put([parent_key | keys], value) do
@ -50,18 +66,31 @@ def put(key, value) do
def delete([key]), do: delete(key) def delete([key]), do: delete(key)
def delete([parent_key | keys]) do def delete([parent_key | keys] = path) do
with {:ok, _} <- fetch(path) do
{_, parent} = {_, parent} =
Application.get_env(:pleroma, parent_key) parent_key
|> get()
|> get_and_update_in(keys, fn _ -> :pop end) |> get_and_update_in(keys, fn _ -> :pop end)
Application.put_env(:pleroma, parent_key, parent) Application.put_env(:pleroma, parent_key, parent)
end end
end
def delete(key) do def delete(key) do
Application.delete_env(:pleroma, key) Application.delete_env(:pleroma, key)
end end
def restrict_unauthenticated_access?(resource, kind) do
setting = get([:restrict_unauthenticated, resource, kind])
if setting in [nil, :if_instance_is_private] do
!get!([:instance, :public])
else
setting
end
end
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []

View File

@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query, only: [select: 3]
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias __MODULE__ alias __MODULE__
@ -14,16 +14,6 @@ defmodule Pleroma.ConfigDB do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@full_key_update [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]
@full_subkey_update [ @full_subkey_update [
{:pleroma, :assets, :mascots}, {:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups}, {:pleroma, :emoji, :groups},
@ -32,14 +22,10 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :mrf_keyword, :replace} {:pleroma, :mrf_keyword, :replace}
] ]
@regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
@delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
schema "config" do schema "config" do
field(:key, :string) field(:key, Pleroma.EctoType.Config.Atom)
field(:group, :string) field(:group, Pleroma.EctoType.Config.Atom)
field(:value, :binary) field(:value, Pleroma.EctoType.Config.BinaryValue)
field(:db, {:array, :string}, virtual: true, default: []) field(:db, {:array, :string}, virtual: true, default: [])
timestamps() timestamps()
@ -51,10 +37,6 @@ def get_all_as_keyword do
|> select([c], {c.group, c.key, c.value}) |> select([c], {c.group, c.key, c.value})
|> Repo.all() |> Repo.all()
|> Enum.reduce([], fn {group, key, value}, acc -> |> Enum.reduce([], fn {group, key, value}, acc ->
group = ConfigDB.from_string(group)
key = ConfigDB.from_string(key)
value = from_binary(value)
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}])) Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end) end)
end end
@ -64,50 +46,41 @@ def get_by_params(params), do: Repo.get_by(ConfigDB, params)
@spec changeset(ConfigDB.t(), map()) :: Changeset.t() @spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do def changeset(config, params \\ %{}) do
params = Map.put(params, :value, transform(params[:value]))
config config
|> cast(params, [:key, :group, :value]) |> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value]) |> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index) |> unique_constraint(:key, name: :config_group_key_index)
end end
@spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} defp create(params) do
def create(params) do
%ConfigDB{} %ConfigDB{}
|> changeset(params) |> changeset(params)
|> Repo.insert() |> Repo.insert()
end end
@spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} defp update(%ConfigDB{} = config, %{value: value}) do
def update(%ConfigDB{} = config, %{value: value}) do
config config
|> changeset(%{value: value}) |> changeset(%{value: value})
|> Repo.update() |> Repo.update()
end end
@spec get_db_keys(ConfigDB.t()) :: [String.t()]
def get_db_keys(%ConfigDB{} = config) do
config.value
|> ConfigDB.from_binary()
|> get_db_keys(config.key)
end
@spec get_db_keys(keyword(), any()) :: [String.t()] @spec get_db_keys(keyword(), any()) :: [String.t()]
def get_db_keys(value, key) do def get_db_keys(value, key) do
keys =
if Keyword.keyword?(value) do if Keyword.keyword?(value) do
value |> Keyword.keys() |> Enum.map(&convert(&1)) Keyword.keys(value)
else else
[convert(key)] [key]
end end
Enum.map(keys, &to_json_types(&1))
end end
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword() @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
def merge_group(group, key, old_value, new_value) do def merge_group(group, key, old_value, new_value) do
new_keys = to_map_set(new_value) new_keys = to_mapset(new_value)
intersect_keys = intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
merged_value = ConfigDB.merge(old_value, new_value) merged_value = ConfigDB.merge(old_value, new_value)
@ -120,12 +93,10 @@ def merge_group(group, key, old_value, new_value) do
[] []
end) end)
|> List.flatten() |> List.flatten()
|> Enum.reduce(merged_value, fn subkey, acc -> |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
Keyword.put(acc, subkey, new_value[subkey])
end)
end end
defp to_map_set(keyword) do defp to_mapset(keyword) do
keyword keyword
|> Keyword.keys() |> Keyword.keys()
|> MapSet.new() |> MapSet.new()
@ -159,57 +130,54 @@ defp deep_merge(_key, value1, value2) do
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do def update_or_create(params) do
params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key]) search_opts = Map.take(params, [:group, :key])
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{:partial_update, true, config} <- {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
{:partial_update, can_be_partially_updated?(config), config}, {_, true, config} <-
old_value <- from_binary(config.value), {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
transformed_value <- do_transform(params[:value]), new_value = merge_group(config.group, config.key, config.value, params[:value])
{:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config}, update(config, %{value: new_value})
new_value <-
merge_group(
ConfigDB.from_string(config.group),
ConfigDB.from_string(config.key),
old_value,
transformed_value
) do
ConfigDB.update(config, %{value: new_value})
else else
{reason, false, config} when reason in [:partial_update, :can_be_merged] -> {reason, false, config} when reason in [:partial_update, :can_be_merged] ->
ConfigDB.update(config, params) update(config, params)
nil -> nil ->
ConfigDB.create(params) create(params)
end end
end end
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config) defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
defp only_full_update?(%ConfigDB{} = config) do defp only_full_update?(%ConfigDB{group: group, key: key}) do
config_group = ConfigDB.from_string(config.group) full_key_update = [
config_key = ConfigDB.from_string(config.key) {:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:swarm, :node_blacklist},
{:logger, :backends}
]
Enum.any?(@full_key_update, fn Enum.any?(full_key_update, fn
{group, key} when is_list(key) -> {s_group, s_key} ->
config_group == group and config_key in key group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
{group, key} ->
config_group == group and config_key == key
end) end)
end end
@spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
def delete(params) do def delete(params) do
search_opts = Map.delete(params, :subkeys) search_opts = Map.delete(params, :subkeys)
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]}, {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
old_value <- from_binary(config.value), keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
keys <- Enum.map(sub_keys, &do_transform_string(&1)), {_, config, new_value} when new_value != [] <-
{:partial_remove, config, new_value} when new_value != [] <- {:partial_remove, config, Keyword.drop(config.value, keys)} do
{:partial_remove, config, Keyword.drop(old_value, keys)} do update(config, %{value: new_value})
ConfigDB.update(config, %{value: new_value})
else else
{:partial_remove, config, []} -> {:partial_remove, config, []} ->
Repo.delete(config) Repo.delete(config)
@ -225,37 +193,32 @@ def delete(params) do
end end
end end
@spec from_binary(binary()) :: term() @spec to_json_types(term()) :: map() | list() | boolean() | String.t()
def from_binary(binary), do: :erlang.binary_to_term(binary) def to_json_types(entity) when is_list(entity) do
Enum.map(entity, &to_json_types/1)
@spec from_binary_with_convert(binary()) :: any()
def from_binary_with_convert(binary) do
binary
|> from_binary()
|> do_convert()
end end
@spec from_string(String.t()) :: atom() | no_return() def to_json_types(%Regex{} = entity), do: inspect(entity)
def from_string(string), do: do_transform_string(string)
@spec convert(any()) :: any() def to_json_types(entity) when is_map(entity) do
def convert(entity), do: do_convert(entity) Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
defp do_convert(entity) when is_list(entity) do
for v <- entity, into: [], do: do_convert(v)
end end
defp do_convert(%Regex{} = entity), do: inspect(entity) def to_json_types({:args, args}) when is_list(args) do
arguments =
Enum.map(args, fn
arg when is_tuple(arg) -> inspect(arg)
arg -> to_json_types(arg)
end)
defp do_convert(entity) when is_map(entity) do %{"tuple" => [":args", arguments]}
for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
end end
defp do_convert({:proxy_url, {type, :localhost, port}}) do def to_json_types({:proxy_url, {type, :localhost, port}}) do
%{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]} %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
end end
defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
ip = ip =
host host
|> :inet_parse.ntoa() |> :inet_parse.ntoa()
@ -264,66 +227,64 @@ defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
%{ %{
"tuple" => [ "tuple" => [
":proxy_url", ":proxy_url",
%{"tuple" => [do_convert(type), ip, port]} %{"tuple" => [to_json_types(type), ip, port]}
] ]
} }
end end
defp do_convert({:proxy_url, {type, host, port}}) do def to_json_types({:proxy_url, {type, host, port}}) do
%{ %{
"tuple" => [ "tuple" => [
":proxy_url", ":proxy_url",
%{"tuple" => [do_convert(type), to_string(host), port]} %{"tuple" => [to_json_types(type), to_string(host), port]}
] ]
} }
end end
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]} def to_json_types({:partial_chain, entity}),
do: %{"tuple" => [":partial_chain", inspect(entity)]}
defp do_convert(entity) when is_tuple(entity) do def to_json_types(entity) when is_tuple(entity) do
value = value =
entity entity
|> Tuple.to_list() |> Tuple.to_list()
|> do_convert() |> to_json_types()
%{"tuple" => value} %{"tuple" => value}
end end
defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do def to_json_types(entity) when is_binary(entity), do: entity
def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
entity entity
end end
defp do_convert(entity) def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
":#{entity}" ":#{entity}"
end end
defp do_convert(entity) when is_atom(entity), do: inspect(entity) def to_json_types(entity) when is_atom(entity), do: inspect(entity)
defp do_convert(entity) when is_binary(entity), do: entity @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
arguments =
Enum.map(args, fn arg ->
if String.contains?(arg, ["{", "}"]) do
{elem, []} = Code.eval_string(arg)
elem
else
to_elixir_types(arg)
end
end)
@spec transform(any()) :: binary() | no_return() {:args, arguments}
def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
entity
|> do_transform()
|> to_binary()
end end
def transform(entity), do: to_binary(entity) def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
{:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
@spec transform_with_out_binary(any()) :: any()
def transform_with_out_binary(entity), do: do_transform(entity)
@spec to_binary(any()) :: binary()
def to_binary(entity), do: :erlang.term_to_binary(entity)
defp do_transform(%Regex{} = entity), do: entity
defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
end end
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} = {partial_chain, []} =
entity entity
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
@ -332,25 +293,51 @@ defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
{:partial_chain, partial_chain} {:partial_chain, partial_chain}
end end
defp do_transform(%{"tuple" => entity}) do def to_elixir_types(%{"tuple" => entity}) do
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
end end
defp do_transform(entity) when is_map(entity) do def to_elixir_types(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)} Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
end end
defp do_transform(entity) when is_list(entity) do def to_elixir_types(entity) when is_list(entity) do
for v <- entity, into: [], do: do_transform(v) Enum.map(entity, &to_elixir_types/1)
end end
defp do_transform(entity) when is_binary(entity) do def to_elixir_types(entity) when is_binary(entity) do
entity entity
|> String.trim() |> String.trim()
|> do_transform_string() |> string_to_elixir_types()
end end
defp do_transform(entity), do: entity def to_elixir_types(entity), do: entity
@spec string_to_elixir_types(String.t()) ::
atom() | Regex.t() | module() | String.t() | no_return()
def string_to_elixir_types("~r" <> _pattern = regex) do
pattern =
~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
Regex.named_captures(pattern, regex),
{:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
{result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
result
end
end
def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
def string_to_elixir_types(value) do
if module_name?(value) do
String.to_existing_atom("Elixir." <> value)
else
value
end
end
defp parse_host("localhost"), do: :localhost defp parse_host("localhost"), do: :localhost
@ -387,27 +374,8 @@ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
end end
end end
defp do_transform_string("~r" <> _pattern = regex) do @spec module_name?(String.t()) :: boolean()
with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- def module_name?(string) do
Regex.named_captures(@regex, regex),
{:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
{result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
result
end
end
defp do_transform_string(":" <> atom), do: String.to_atom(atom)
defp do_transform_string(value) do
if is_module_name?(value) do
String.to_existing_atom("Elixir." <> value)
else
value
end
end
@spec is_module_name?(String.t()) :: boolean()
def is_module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger"] string in ["Oban", "Ueberauth", "ExSyslogger"]
end end

View File

@ -3,9 +3,23 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do defmodule Pleroma.Config.DeprecationWarnings do
alias Pleroma.Config
require Logger require Logger
alias Pleroma.Config alias Pleroma.Config
@type config_namespace() :: [atom()]
@type config_map() :: {config_namespace(), config_namespace(), String.t()}
@mrf_config_map [
{[:instance, :rewrite_policy], [:mrf, :policies],
"\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"},
{[:instance, :mrf_transparency], [:mrf, :transparency],
"\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"},
{[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions],
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
]
def check_hellthread_threshold do def check_hellthread_threshold do
if Config.get([:mrf_hellthread, :threshold]) do if Config.get([:mrf_hellthread, :threshold]) do
Logger.warn(""" Logger.warn("""
@ -39,5 +53,66 @@ def mrf_user_allowlist do
def warn do def warn do
check_hellthread_threshold() check_hellthread_threshold()
mrf_user_allowlist() mrf_user_allowlist()
check_old_mrf_config()
check_media_proxy_whitelist_config()
check_welcome_message_config()
end
def check_welcome_message_config do
instance_config = Pleroma.Config.get([:instance])
use_old_config =
Keyword.has_key?(instance_config, :welcome_user_nickname) or
Keyword.has_key?(instance_config, :welcome_message)
if use_old_config do
Logger.error("""
!!!DEPRECATION WARNING!!!
Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace:
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
""")
end
end
def check_old_mrf_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later:
"""
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
{old, new, err_msg}, acc ->
old_config = Config.get(old)
if old_config do
Config.put(new, old_config)
acc <> err_msg
else
acc
end
end)
if warning != "" do
Logger.warn(warning_preface <> warning)
end
end
@spec check_media_proxy_whitelist_config() :: :ok | nil
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
""")
end
end end
end end

Some files were not shown because too many files have changed in this diff Show More