Merge remote-tracking branch 'remotes/upstream/develop' into 1260-rate-limited-auth-actions

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Ivan Tashkinov 2019-10-06 09:30:49 +03:00
commit f459aabdfa
170 changed files with 3158 additions and 2066 deletions

View File

@ -28,23 +28,6 @@ build:
- mix deps.get - mix deps.get
- mix compile --force - mix compile --force
docs-build:
stage: build
only:
- master@pleroma/pleroma
- develop@pleroma/pleroma
variables:
MIX_ENV: dev
PLEROMA_BUILD_ENV: prod
script:
- mix deps.get
- mix compile
- mix docs
artifacts:
paths:
- priv/static/doc
unit-testing: unit-testing:
stage: test stage: test
services: services:
@ -85,19 +68,14 @@ analysis:
docs-deploy: docs-deploy:
stage: deploy stage: deploy
image: alpine:3.9 image: alpine:latest
only: only:
- master@pleroma/pleroma - master@pleroma/pleroma
- develop@pleroma/pleroma - develop@pleroma/pleroma
before_script: before_script:
- apk update && apk add openssh-client rsync - apk add curl
script: script:
- mkdir -p ~/.ssh - curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline
- echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}"
review_app: review_app:
image: alpine:3.9 image: alpine:3.9
stage: deploy stage: deploy
@ -151,6 +129,7 @@ amd64:
only: &release-only only: &release-only
- master@pleroma/pleroma - master@pleroma/pleroma
- develop@pleroma/pleroma - develop@pleroma/pleroma
- /^maint/.*$/@pleroma/pleroma
artifacts: &release-artifacts artifacts: &release-artifacts
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME" name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
paths: paths:

View File

@ -6,10 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Refreshing poll results for remote polls - Refreshing poll results for remote polls
- Job queue stats to the healthcheck page
- Admin API: Add ability to require password reset - Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition) - Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items - Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item - Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
- Authentication: Added rate limit for password-authorized actions / login existence checks
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@ -22,6 +26,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- Added `:instance, extended_nickname_format` setting to the default config
## [1.1.0] - 2019-??-?? ## [1.1.0] - 2019-??-??
### Security ### Security
@ -73,6 +79,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- ActivityPub: Deactivated user deletion - ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user - ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled - MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
### Added ### Added
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically. - Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
@ -117,7 +124,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`) - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter. - ActivityPub: Add ActivityPub actor's `discoverable` parameter.
- Admin API: Added moderation log filters (user/start date/end date/search/pagination) - Admin API: Added moderation log filters (user/start date/end date/search/pagination)
- Authentication: Added rate limit for password-authorized actions / login existence checks. - Reverse Proxy: Do not retry failed requests to limit pressure on the peer
### Changed ### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text

View File

@ -279,7 +279,8 @@
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,
external_user_synchronization: true external_user_synchronization: true,
extended_nickname_format: false
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because

View File

@ -56,6 +56,7 @@ Has these additional fields under the `pleroma` object:
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
- `deactivated`: boolean, true when the user is deactivated - `deactivated`: boolean, true when the user is deactivated
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
### Source ### Source

View File

@ -124,7 +124,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
``` ```
## `/api/pleroma/admin/` ## `/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`
### Mark notifications as read ### Mark notifications as read
@ -317,7 +317,8 @@ See [Admin-API](Admin-API.md)
"active": 0, # active processes "active": 0, # active processes
"idle": 0, # idle processes "idle": 0, # idle processes
"memory_used": 0.00, # Memory used "memory_used": 0.00, # Memory used
"healthy": true # Instance state "healthy": true, # Instance state
"job_queue_stats": {} # Job queue stats
} }
``` ```

View File

@ -0,0 +1,19 @@
# Transfering the config to/from the database
!!! danger
This is a Work In Progress, not usable just yet.
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl config` and in case of source installs it's
`mix pleroma.config`.
## Transfer config from file to DB.
```sh
$PREFIX migrate_to_db
```
## Transfer config from DB to `config/env.exported_from_db.secret.exs`
```sh
$PREFIX migrate_from_db <env>
```

View File

@ -0,0 +1,48 @@
# Database maintenance tasks
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`.
## Replace embedded objects with their references
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
$PREFIX remove_embedded_objects [<options>]
```
### Options
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
## Prune old remote posts from the database
This will prune remote posts older than 90 days (configurable with [`config :pleroma, :instance, remote_post_retention_days`](../../configuration/cheatsheet.md#instance)) from the database, they will be refetched from source when accessed.
!!! note
The disk space will only be reclaimed after `VACUUM FULL`
```sh
$PREFIX pleroma.database prune_objects [<options>]
```
### Options
- `--vacuum` - run `VACUUM FULL` after the objects are pruned
## Create a conversation for all existing DMs
Can be safely re-run
```sh
$PREFIX bump_all_conversations
```
## Remove duplicated items from following and update followers count for all users
```sh
$PREFIX update_users_following_followers_counts
```
## Fix the pre-existing "likes" collections for all objects
```sh
$PREFIX fix_likes_collections
```

View File

@ -0,0 +1,13 @@
# Managing digest emails
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl digest` and in case of source installs it's `mix pleroma.digest`.
## Send digest email since given date (user registration date by default) ignoring user activity status.
```sh
$PREFIX test <nickname> [<since_date>]
```
Example:
```sh
$PREFIX test donaldtheduck 2019-05-20
```

View File

@ -0,0 +1,30 @@
# Managing emoji packs
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl emoji` and in case of source installs it's `mix pleroma.emoji`.
## Lists emoji packs and metadata specified in the manifest
```sh
$PREFIX ls-packs [<options>]
```
### Options
- `-m, --manifest PATH/URL` - path to a custom manifest, it can either be an URL starting with `http`, in that case the manifest will be fetched from that address, or a local path
## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME`
```sh
$PREFIX get-packs [<options>] <packs>
```
### Options
- `-m, --manifest PATH/URL` - same as [`ls-packs`](#ls-packs)
## Create a new manifest entry and a file list from the specified remote pack file
```sh
$PREFIX 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.
The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).

View File

@ -0,0 +1,30 @@
# Managing instance configuration
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl instance` and in case of source installs it's `mix pleroma.instance`.
## Generate a new configuration file
```sh
$PREFIX gen [<options>]
```
If any of the options are left unspecified, you will be prompted interactively.
### Options
- `-f`, `--force` - overwrite any output files
- `-o <path>`, `--output <path>` - the output file for the generated configuration
- `--output-psql <path>` - the output file for the generated PostgreSQL setup
- `--domain <domain>` - the domain of your instance
- `--instance-name <instance_name>` - the name of your instance
- `--admin-email <email>` - the email address of the instance admin
- `--notify-email <email>` - email address for notifications
- `--dbhost <hostname>` - the hostname of the PostgreSQL database to use
- `--dbname <database_name>` - the name of the database to use
- `--dbuser <username>` - the user (aka role) to use for the database connection
- `--dbpass <password>` - the password to use for the database connection
- `--rum <Y|N>` - Whether to enable RUM indexes
- `--indexable <Y|N>` - Allow/disallow indexing site by search engines
- `--db-configurable <Y|N>` - Allow/disallow configuring instance from admin part
- `--uploads-dir <path>` - the directory uploads go in when using a local uploader
- `--static-dir <path>` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
- `--listen-ip <ip>` - the ip the app should listen to, defaults to 127.0.0.1
- `--listen-port <port>` - the port the app should listen to, defaults to 4000

View File

@ -0,0 +1,30 @@
# Managing relays
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl relay` and in case of source installs it's `mix pleroma.relay`.
## Follow a relay
```sh
$PREFIX follow <relay_url>
```
Example:
```sh
$PREFIX follow https://example.org/relay
```
## Unfollow a remote relay
```sh
$PREFIX unfollow <relay_url>
```
Example:
```sh
$PREFIX unfollow https://example.org/relay
```
## List relay subscriptions
```sh
$PREFIX list
```

View File

@ -0,0 +1,12 @@
# Managing uploads
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl uploads` and in case of source installs it's `mix pleroma.uploads`.
## Migrate uploads from local to remote storage
```sh
$PREFIX migrate_local <target_uploader> [<options>]
```
### Options
- `--delete` - delete local uploads after migrating them to the target uploader
A list of available uploaders can be seen in [Configuration Cheat Sheet](../../configuration/cheatsheet.md#pleromaupload)

View File

@ -0,0 +1,94 @@
# Managing users
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl user` and in case of source installs it's `mix pleroma.user`.
## Create a user
```sh
$PREFIX new <nickname> <email> [<options>]
```
### Options
- `--name <name>` - the user's display name
- `--bio <bio>` - the user's bio
- `--password <password>` - the user's password
- `--moderator`/`--no-moderator` - whether the user should be a moderator
- `--admin`/`--no-admin` - whether the user should be an admin
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## Generate an invite link
```sh
$PREFIX invite [<options>]
```
### Options
- `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05")
- `--max-use NUMBER` - maximum numbers of token uses
## List generated invites
```sh
$PREFIX invites
```
## Revoke invite
```sh
$PREFIX revoke_invite <token_or_id>
```
## Delete a user
```sh
$PREFIX rm <nickname>
```
## Delete user's posts and interactions
```sh
$PREFIX delete_activities <nickname>
```
## Sign user out from all applications (delete user's OAuth tokens and authorizations)
```sh
$PREFIX sign_out <nickname>
```
## Deactivate or activate a user
```sh
$PREFIX toggle_activated <nickname>
```
## Unsubscribe local users from a user and deactivate the user
```sh
$PREFIX unsubscribe NICKNAME
```
## Unsubscribe local users from an instance and deactivate all accounts on it
```sh
$PREFIX unsubscribe_all_from_instance <instance>
```
## Create a password reset link for user
```sh
$PREFIX reset_password <nickname>
```
## Set the value of the given user's settings
```sh
$PREFIX set <nickname> [<options>]
```
### Options
- `--locked`/`--no-locked` - whether the user should be locked
- `--moderator`/`--no-moderator` - whether the user should be a moderator
- `--admin`/`--no-admin` - whether the user should be an admin
## Add tags to a user
```sh
$PREFIX tag <nickname> <tags>
```
## Delete tags from a user
```sh
$PREFIX untag <nickname> <tags>
```
## Toggle confirmation status of the user
```sh
$PREFIX toggle_confirmed <nickname>
```

View File

@ -1,17 +0,0 @@
# General tips for customizing Pleroma FE
There are some configuration scripts for Pleroma BE and FE:
1. `config/prod.secret.exs`
1. `config/config.exs`
1. `priv/static/static/config.json`
The `prod.secret.exs` affects first. `config.exs` is for fallback or default. `config.json` is for GNU-social-BE-Pleroma-FE instances.
Usually all you have to do is:
1. Copy the section in the `config/config.exs` which you want to activate.
1. Paste into `config/prod.secret.exs`.
1. Edit `config/prod.secret.exs`.
1. Restart the Pleroma daemon.
`prod.secret.exs` is for the `MIX_ENV=prod` environment. `dev.secret.exs` is for the `MIX_ENV=dev` environment respectively.

View File

@ -1,12 +0,0 @@
# Small customizations
See also static_dir.md for visual settings.
## Theme
All users of your instance will be able to change the theme they use by going to the settings (the cog in the top-right hand corner). However, if you wish to change the default theme, you can do so by editing `theme` in `config/dev.secret.exs` accordingly.
## Message Visibility
To enable message visibility options when posting like in the Mastodon frontend, set
`scope_options_enabled` to `true` in `config/dev.secret.exs`.

View File

@ -1,7 +1,11 @@
# Configuration # Configuration Cheat Sheet
This is a cheat sheet for Pleroma configuration file, any setting possible to configure should be listed here.
Pleroma configuration works by first importing the base config (`config/config.exs` on source installs, compiled-in on OTP releases), then overriding it by the environment config (`config/$MIX_ENV.exs` on source installs, N/A to OTP releases) and then overriding it by user config (`config/$MIX_ENV.secret.exs` on source installs, typically `/etc/pleroma/config.exs` on OTP releases).
You shouldn't edit the base config directly to avoid breakages and merge conflicts, but it can be used as a reference if you don't understand how an option is supposed to be formatted, the latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs).
This file describe the configuration, it is recommended to edit the relevant *.secret.exs file instead of the others founds in the ``config`` directory.
If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherwise it is ``dev.secret.exs``.
## Pleroma.Upload ## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use * `uploader`: Select which `Pleroma.Uploaders` to use
@ -11,7 +15,8 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
* `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.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. !!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
## 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
@ -111,12 +116,6 @@ config :pleroma, Pleroma.Emails.Mailer,
* `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`: 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. * `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
* `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default.
* `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values:
* "email": Copy and preprend re:, as in email.
* "masto": Copy verbatim, as in Mastodon.
* "noop": Don't copy the subject.
* `always_show_subject_input`: When set to false, auto-hide the subject field when it's empty.
* `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.
@ -132,13 +131,17 @@ config :pleroma, Pleroma.Emails.Mailer,
* `user_name_length`: A user name maximum length (default: `100`) * `user_name_length`: A user name maximum length (default: `100`)
* `skip_thread_containment`: Skip filter out broken threads. The default is `false`. * `skip_thread_containment`: Skip filter out broken threads. The default is `false`.
* `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`. * `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`.
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
* `max_account_fields`: The maximum number of custom fields in the user profile (default: `10`) * `max_account_fields`: The maximum number of custom fields in the user profile (default: `10`)
* `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`)
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users. * `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
!!! danger
This is a Work In Progress, not usable just yet
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
## :logger ## :logger
@ -186,7 +189,7 @@ See the [Quack Github](https://github.com/azohra/quack) for more details
## :frontend_configurations ## :frontend_configurations
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options).
Frontends can access these settings at `/api/pleroma/frontend_configurations` Frontends can access these settings at `/api/pleroma/frontend_configurations`
@ -208,14 +211,15 @@ These settings **need to be complete**, they will override the defaults.
NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below. NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below.
## :fe ## :fe
__THIS IS DEPRECATED__ !!! warning
__THIS IS DEPRECATED__
If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method. If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method.
Please **set this option to false** in your config like this: Please **set this option to false** in your config like this:
```elixir ```elixir
config :pleroma, :fe, false config :pleroma, :fe, false
``` ```
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false. This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
@ -261,7 +265,7 @@ All criteria are configured as a map of regular expressions to lists of policy m
Example: Example:
``` ```elixir
config :pleroma, :mrf_subchain, config :pleroma, :mrf_subchain,
match_actor: %{ match_actor: %{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy] ~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
@ -301,7 +305,10 @@ config :pleroma, :mrf_subchain,
* `dstport`: Port advertised in urls (optional, defaults to `port`) * `dstport`: Port advertised in urls (optional, defaults to `port`)
## Pleroma.Web.Endpoint ## Pleroma.Web.Endpoint
`Phoenix` endpoint configuration, all configuration options can be viewed [here](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-dynamic-configuration), only common options are listed here
!!! note
`Phoenix` endpoint configuration, all configuration options can be viewed [here](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-dynamic-configuration), only common options are listed here.
* `http` - a list containing http protocol configuration, all configuration options can be viewed [here](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html#module-options), only common options are listed here. For deployment using docker, you need to set this to `[ip: {0,0,0,0}, port: 4000]` to make pleroma accessible from other containers (such as your nginx server). * `http` - a list containing http protocol configuration, all configuration options can be viewed [here](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html#module-options), only common options are listed here. For deployment using docker, you need to set this to `[ip: {0,0,0,0}, port: 4000]` to make pleroma accessible from other containers (such as your nginx server).
- `ip` - a tuple consisting of 4 integers - `ip` - a tuple consisting of 4 integers
- `port` - `port`
@ -314,7 +321,8 @@ config :pleroma, :mrf_subchain,
**Important note**: if you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need !!! warning
If you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need
Example: Example:
```elixir ```elixir
@ -440,11 +448,6 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`.
`config :pleroma_job_queue, :queues` is replaced by `config :pleroma, Oban, :queues` and uses the same format (keys are queues' names, values are max concurrent jobs numbers). `config :pleroma_job_queue, :queues` is replaced by `config :pleroma, Oban, :queues` and uses the same format (keys are queues' names, values are max concurrent jobs numbers).
### Note on running with PostgreSQL in silent mode
If you are running PostgreSQL in [`silent_mode`](https://postgresqlco.nf/en/doc/param/silent_mode?version=9.1), it's advised to set [`log_destination`](https://postgresqlco.nf/en/doc/param/log_destination?version=9.1) to `syslog`,
otherwise `postmaster.log` file may grow because of "you don't own a lock of type ShareLock" warnings (see https://github.com/sorentwo/oban/issues/52).
## :workers ## :workers
Includes custom worker options not interpretable directly by `Oban`. Includes custom worker options not interpretable directly by `Oban`.
@ -552,7 +555,7 @@ The above example defines a single job which invokes `Pleroma.Web.Websub.refresh
## Pleroma.ActivityExpiration ## Pleroma.ActivityExpiration
# `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
## Pleroma.Web.Auth.Authenticator ## Pleroma.Web.Auth.Authenticator
@ -628,13 +631,14 @@ Email notifications settings.
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.).
Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies).
Note: each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, !!! note
e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. Each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies.
The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies.
Note: each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies. !!! note
Each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies.
Note: make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you have this feature enabled. OAuth consumer mode will not work with `"SameSite=Strict"` !!! note
Make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you have this feature enabled. OAuth consumer mode will not work with `"SameSite=Strict"`
* For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback * For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback
@ -739,8 +743,6 @@ A keyword list of rate limiters where a key is a limiter name and value is the l
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
See [`Pleroma.Plugs.RateLimiter`](Pleroma.Plugs.RateLimiter.html) documentation for examples.
Supported rate limiters: Supported rate limiters:
* `:search` for the search requests (account & status search etc.) * `:search` for the search requests (account & status search etc.)
@ -761,7 +763,8 @@ Available caches:
## Pleroma.Plugs.RemoteIp ## Pleroma.Plugs.RemoteIp
**If your instance is not behind at least one reverse proxy, you should not enable this plug.** !!! warning
If your instance is not behind at least one reverse proxy, you should not enable this plug.
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.

View File

@ -4,6 +4,7 @@ Before you add your own custom emoji, check if they are available in an existing
See `Mix.Tasks.Pleroma.Emoji` for information about emoji packs. See `Mix.Tasks.Pleroma.Emoji` for information about emoji packs.
To add custom emoji: To add custom emoji:
* Create the `STATIC-DIR/emoji/` directory if it doesn't exist * Create the `STATIC-DIR/emoji/` directory if it doesn't exist
(`STATIC-DIR` is configurable, `instance/static/` by default) (`STATIC-DIR` is configurable, `instance/static/` by default)
* Create a directory with whatever name you want (custom is a good name to show the purpose of it). * Create a directory with whatever name you want (custom is a good name to show the purpose of it).

View File

@ -225,12 +225,10 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## Questions ## Questions

View File

@ -200,12 +200,10 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## Questions ## Questions

View File

@ -264,12 +264,10 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## Questions ## Questions

View File

@ -190,12 +190,10 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## Questions ## Questions

View File

@ -179,12 +179,10 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### その他の設定とカスタマイズ #### その他の設定とカスタマイズ
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## 質問ある? ## 質問ある?

View File

@ -283,12 +283,10 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
#### Further reading #### Further reading
* [Backup your instance](backup.html) * [Backup your instance](../administration/backup.md)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Hardening your instance](../configuration/hardening.md)
* [Hardening your instance](hardening.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [How to activate mediaproxy](howto_mediaproxy.html) * [Updating your instance](../administration/updating.md)
* [Small Pleroma-FE customizations](small_customizations.html)
* [Updating your instance](updating.html)
## Questions ## Questions

View File

@ -11,7 +11,7 @@ Benefits of OTP releases over from-source installs include:
* **Faster and less bug-prone mix tasks.** On a from-source install one has to wait untill a new Pleroma node is started for each mix task and they execute outside of the instance context (for example if a user was deleted via a mix task, the instance will have no knowledge of that and continue to display status count and follows before the cache expires). Mix tasks in OTP releases are executed by calling into a running instance via RPC, which solves both of these problems. * **Faster and less bug-prone mix tasks.** On a from-source install one has to wait untill a new Pleroma node is started for each mix task and they execute outside of the instance context (for example if a user was deleted via a mix task, the instance will have no knowledge of that and continue to display status count and follows before the cache expires). Mix tasks in OTP releases are executed by calling into a running instance via RPC, which solves both of these problems.
### Sounds great, how do I switch? ### Sounds great, how do I switch?
Currently we support Linux machines with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPUs. If you are unsure, check the [Detecting flavour](otp_en.html#detecting-flavour) section in OTP install guide. If your platform is supported, proceed with the guide, if not check the [My platform is not supported](#my-platform-is-not-supported) section. Currently we support Linux machines with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPUs. If you are unsure, check the [Detecting flavour](otp_en.md#detecting-flavour) section in OTP install guide. If your platform is supported, proceed with the guide, if not check the [My platform is not supported](#my-platform-is-not-supported) section.
### I don't think it is worth the effort, can I stay on a from-source install? ### I don't think it is worth the effort, can I stay on a from-source install?
Yes, currently there are no plans to deprecate them. Yes, currently there are no plans to deprecate them.
@ -70,7 +70,7 @@ and then copy custom emojis to `/var/lib/pleroma/static/emoji/custom`.
This is needed because storing custom emojis in the root directory is deprecated, but if you just move them to `/var/lib/pleroma/static/emoji/custom` it will break emoji urls on old posts. This is needed because storing custom emojis in the root directory is deprecated, but if you just move them to `/var/lib/pleroma/static/emoji/custom` it will break emoji urls on old posts.
Note that globs have been replaced with `pack_extensions`, so if your emojis are not in png/gif you should [modify the default value](config.html#emoji). Note that globs have been replaced with `pack_extensions`, so if your emojis are not in png/gif you should [modify the default value](../configuration/cheatsheet.md#emoji).
### Moving the config ### Moving the config
```sh ```sh
@ -86,7 +86,7 @@ mv ~pleroma/config/prod.secret.exs /etc/pleroma/config.exs
$EDITOR /etc/pleroma/config.exs $EDITOR /etc/pleroma/config.exs
``` ```
## Installing the release ## Installing the release
Before proceeding, get the flavour from [Detecting flavour](otp_en.html#detecting-flavour) section in OTP installation guide. Before proceeding, get the flavour from [Detecting flavour](otp_en.md#detecting-flavour) section in OTP installation guide.
```sh ```sh
# Delete all files in pleroma user's directory # Delete all files in pleroma user's directory
rm -r ~pleroma/* rm -r ~pleroma/*
@ -148,6 +148,6 @@ cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma
rc-service pleroma start rc-service pleroma start
``` ```
## Running mix tasks ## Running mix tasks
Refer to [Running mix tasks](otp_en.html#running-mix-tasks) section from OTP release installation guide. Refer to [Running mix tasks](otp_en.md#running-mix-tasks) section from OTP release installation guide.
## Updating ## Updating
Refer to [Updating](otp_en.html#updating) section from OTP release installation guide. Refer to [Updating](otp_en.md#updating) section from OTP release installation guide.

View File

@ -42,7 +42,7 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
## Setup ## Setup
### Configuring PostgreSQL ### Configuring PostgreSQL
#### (Optional) Installing RUM indexes #### (Optional) Installing RUM indexes
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](config.html#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).
Debian/Ubuntu (available only on Buster/19.04): Debian/Ubuntu (available only on Buster/19.04):
```sh ```sh
@ -262,8 +262,8 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
But you should **always check the release notes/changelog** in case there are config deprecations, special update steps, etc. But you should **always check the release notes/changelog** in case there are config deprecations, special update steps, etc.
## Further reading ## Further reading
* [Configuration](config.html)
* [Pleroma's base config.exs](https://git.pleroma.social/pleroma/pleroma/blob/master/config/config.exs) * [Backup your instance](../administration/backup.md)
* [Hardening your instance](hardening.html) * [Hardening your instance](../configuration/hardening.md)
* [Pleroma Clients](clients.html) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
* [Emoji pack manager](Mix.Tasks.Pleroma.Emoji.html) * [Updating your instance](../administration/updating.md)

View File

@ -8,18 +8,7 @@ defmodule Mix.Tasks.Pleroma.Config do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
@shortdoc "Manages the location of the config" @shortdoc "Manages the location of the config"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/config.md")
Manages the location of the config.
## Transfers config from file to DB.
mix pleroma.config migrate_to_db
## Transfers config from DB to file `config/env.exported_from_db.secret.exs`
mix pleroma.config migrate_from_db ENV
"""
def run(["migrate_to_db"]) do def run(["migrate_to_db"]) do
start_pleroma() start_pleroma()

View File

@ -13,34 +13,8 @@ defmodule Mix.Tasks.Pleroma.Database do
use Mix.Task use Mix.Task
@shortdoc "A collection of database related tasks" @shortdoc "A collection of database related tasks"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/database.md")
A collection of database related tasks
## Replace embedded objects with their references
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
mix pleroma.database remove_embedded_objects
Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
## Prune old objects from the database
mix pleroma.database prune_objects
## Create a conversation for all existing DMs. Can be safely re-run.
mix pleroma.database bump_all_conversations
## Remove duplicated items from following and update followers count for all users
mix pleroma.database update_users_following_followers_counts
## Fix the pre-existing "likes" collections for all objects
mix pleroma.database fix_likes_collections
"""
def run(["remove_embedded_objects" | args]) do def run(["remove_embedded_objects" | args]) do
{options, [], []} = {options, [], []} =
OptionParser.parse( OptionParser.parse(

View File

@ -2,16 +2,8 @@ defmodule Mix.Tasks.Pleroma.Digest do
use Mix.Task use Mix.Task
@shortdoc "Manages digest emails" @shortdoc "Manages digest emails"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
Manages digest emails
## Send digest email since given date (user registration date by default)
ignoring user activity status.
``mix pleroma.digest test <nickname> <since_date>``
Example: ``mix pleroma.digest test donaldtheduck 2019-05-20``
"""
def run(["test", nickname | opts]) do def run(["test", nickname | opts]) do
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()

View File

@ -6,54 +6,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task use Mix.Task
@shortdoc "Manages emoji packs" @shortdoc "Manages emoji packs"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")
Manages emoji packs
## ls-packs
mix pleroma.emoji ls-packs [OPTION...]
Lists the emoji packs and metadata specified in the manifest.
### Options
- `-m, --manifest PATH/URL` - path to a custom manifest, it can
either be an URL starting with `http`, in that case the
manifest will be fetched from that address, or a local path
## get-packs
mix pleroma.emoji get-packs [OPTION...] PACKS
Fetches, verifies and installs the specified PACKS from the
manifest into the `STATIC-DIR/emoji/PACK-NAME`
### Options
- `-m, --manifest PATH/URL` - same as ls-packs
## gen-pack
mix pleroma.emoji gen-pack PACK-URL
Creates a new manifest entry and a file list from the specified
remote pack file. Currently, only .zip archives are recognized
as remote pack files and packs are therefore assumed to be zip
archives. This command is intended to run interactively and will
first ask you some basic questions about the pack, then download
the remote file and generate an SHA256 checksum for it, then
generate an emoji file list for you.
The manifest entry will either be written to a newly created
`index.json` file or appended to the existing one, *replacing*
the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously,
*replacing* that file. You _should_ check that the file list doesn't
contain anything you don't need in the pack, that is, anything that is
not an emoji (the whole pack is downloaded, but only emoji files
are extracted).
"""
def run(["ls-packs" | args]) do def run(["ls-packs" | args]) do
Application.ensure_all_started(:hackney) Application.ensure_all_started(:hackney)

View File

@ -7,36 +7,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
import Mix.Pleroma import Mix.Pleroma
@shortdoc "Manages Pleroma instance" @shortdoc "Manages Pleroma instance"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/instance.md")
Manages Pleroma instance.
## Generate a new instance config.
mix pleroma.instance gen [OPTION...]
If any options are left unspecified, you will be prompted interactively
## Options
- `-f`, `--force` - overwrite any output files
- `-o PATH`, `--output PATH` - the output file for the generated configuration
- `--output-psql PATH` - the output file for the generated PostgreSQL setup
- `--domain DOMAIN` - the domain of your instance
- `--instance-name INSTANCE_NAME` - the name of your instance
- `--admin-email ADMIN_EMAIL` - the email address of the instance admin
- `--notify-email NOTIFY_EMAIL` - email address for notifications
- `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use
- `--dbname DBNAME` - the name of the database to use
- `--dbuser DBUSER` - the user (aka role) to use for the database connection
- `--dbpass DBPASS` - the password to use for the database connection
- `--rum Y/N` - Whether to enable RUM indexes
- `--indexable Y/N` - Allow/disallow indexing site by search engines
- `--db-configurable Y/N` - Allow/disallow configuring instance from admin part
- `--uploads-dir` - the directory uploads go in when using a local uploader
- `--static-dir` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
- `--listen-ip` - the ip the app should listen to, defaults to 127.0.0.1
- `--listen-port` - the port the app should listen to, defaults to 4000
"""
def run(["gen" | rest]) do def run(["gen" | rest]) do
{options, [], []} = {options, [], []} =

View File

@ -9,25 +9,8 @@ defmodule Mix.Tasks.Pleroma.Relay do
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Manages remote relays" @shortdoc "Manages remote relays"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/relay.md")
Manages remote relays
## Follow a remote relay
``mix pleroma.relay follow <relay_url>``
Example: ``mix pleroma.relay follow https://example.org/relay``
## Unfollow a remote relay
``mix pleroma.relay unfollow <relay_url>``
Example: ``mix pleroma.relay unfollow https://example.org/relay``
## List relay subscriptions
``mix pleroma.relay list``
"""
def run(["follow", target]) do def run(["follow", target]) do
start_pleroma() start_pleroma()

View File

@ -12,16 +12,8 @@ defmodule Mix.Tasks.Pleroma.Uploads do
@log_every 50 @log_every 50
@shortdoc "Migrates uploads from local to remote storage" @shortdoc "Migrates uploads from local to remote storage"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/uploads.md")
Manages uploads
## Migrate uploads from local to remote storage
mix pleroma.uploads migrate_local TARGET_UPLOADER [OPTIONS...]
Options:
- `--delete` - delete local uploads after migrating them to the target uploader
A list of available uploaders can be seen in config.exs
"""
def run(["migrate_local", target_uploader | args]) do def run(["migrate_local", target_uploader | args]) do
delete? = Enum.member?(args, "--delete") delete? = Enum.member?(args, "--delete")
start_pleroma() start_pleroma()

View File

@ -10,86 +10,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Pleroma.Web.OAuth alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc """ @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
Manages Pleroma users.
## Create a new user.
mix pleroma.user new NICKNAME EMAIL [OPTION...]
Options:
- `--name NAME` - the user's name (i.e., "Lain Iwakura")
- `--bio BIO` - the user's bio
- `--password PASSWORD` - the user's password
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## Generate an invite link.
mix pleroma.user invite [OPTION...]
Options:
- `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05")
- `--max-use NUMBER` - maximum numbers of token uses
## List generated invites
mix pleroma.user invites
## Revoke invite
mix pleroma.user revoke_invite TOKEN OR TOKEN_ID
## Delete the user's account.
mix pleroma.user rm NICKNAME
## Delete the user's activities.
mix pleroma.user delete_activities NICKNAME
## Sign user out from all applications (delete user's OAuth tokens and authorizations).
mix pleroma.user sign_out NICKNAME
## Deactivate or activate the user's account.
mix pleroma.user toggle_activated NICKNAME
## Unsubscribe local users from user's account and deactivate it
mix pleroma.user unsubscribe NICKNAME
## Unsubscribe local users from an entire instance and deactivate all accounts
mix pleroma.user unsubscribe_all_from_instance INSTANCE
## Create a password reset link.
mix pleroma.user reset_password NICKNAME
## Set the value of the given user's settings.
mix pleroma.user set NICKNAME [OPTION...]
Options:
- `--locked`/`--no-locked` - whether the user's account is locked
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
## Add tags to a user.
mix pleroma.user tag NICKNAME TAGS
## Delete tags from a user.
mix pleroma.user untag NICKNAME TAGS
## Toggle confirmation of the user's account.
mix pleroma.user toggle_confirmed NICKNAME
"""
def run(["new", nickname, email | rest]) do def run(["new", nickname, email | rest]) do
{options, [], []} = {options, [], []} =
OptionParser.parse( OptionParser.parse(

View File

@ -42,6 +42,7 @@ def start(_type, _args) do
hackney_pool_children() ++ hackney_pool_children() ++
[ [
Pleroma.Stats, Pleroma.Stats,
Pleroma.JobQueueMonitor,
{Oban, Pleroma.Config.get(Oban)} {Oban, Pleroma.Config.get(Oban)}
] ++ ] ++
task_children(@env) ++ task_children(@env) ++
@ -102,7 +103,8 @@ defp cachex_children do
build_cachex("scrubber", limit: 2500), build_cachex("scrubber", limit: 2500),
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)
] ]
end end

View File

@ -67,6 +67,8 @@ def create_or_bump_for(activity, opts \\ []) do
participations = participations =
Enum.map(users, fn user -> Enum.map(users, fn user ->
User.increment_unread_conversation_count(conversation, user)
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts) Participation.create_for_user_and_conversation(user, conversation, opts)

View File

@ -52,6 +52,15 @@ def mark_as_read(participation) do
participation participation
|> read_cng(%{read: true}) |> read_cng(%{read: true})
|> Repo.update() |> Repo.update()
|> case do
{:ok, participation} ->
participation = Repo.preload(participation, :user)
User.set_unread_conversation_count(participation.user)
{:ok, participation}
error ->
error
end
end end
def mark_as_unread(participation) do def mark_as_unread(participation) do
@ -135,4 +144,12 @@ def set_recipients(participation, user_ids) do
{:ok, Repo.preload(participation, :recipients, force: true)} {:ok, Repo.preload(participation, :recipients, force: true)}
end end
def unread_conversation_count_for_user(user) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
where: not p.read,
select: %{count: count(p.id)}
)
end
end end

View File

@ -14,6 +14,7 @@ defmodule Pleroma.Healthcheck do
active: 0, active: 0,
idle: 0, idle: 0,
memory_used: 0, memory_used: 0,
job_queue_stats: nil,
healthy: true healthy: true
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -21,6 +22,7 @@ defmodule Pleroma.Healthcheck do
active: non_neg_integer(), active: non_neg_integer(),
idle: non_neg_integer(), idle: non_neg_integer(),
memory_used: number(), memory_used: number(),
job_queue_stats: map(),
healthy: boolean() healthy: boolean()
} }
@ -30,6 +32,7 @@ def system_info do
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2) memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
} }
|> assign_db_info() |> assign_db_info()
|> assign_job_queue_stats()
|> check_health() |> check_health()
end end
@ -55,6 +58,11 @@ defp assign_db_info(healthcheck) do
Map.merge(healthcheck, db_info) Map.merge(healthcheck, db_info)
end end
defp assign_job_queue_stats(healthcheck) do
stats = Pleroma.JobQueueMonitor.stats()
Map.put(healthcheck, :job_queue_stats, stats)
end
@spec check_health(Healthcheck.t()) :: Healthcheck.t() @spec check_health(Healthcheck.t()) :: Healthcheck.t()
def check_health(%{pool_size: pool_size, active: active} = check) def check_health(%{pool_size: pool_size, active: active} = check)
when active >= pool_size do when active >= pool_size do

View File

@ -0,0 +1,78 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.JobQueueMonitor do
use GenServer
@initial_state %{workers: %{}, queues: %{}, processed_jobs: 0}
@queue %{processed_jobs: 0, success: 0, failure: 0}
@operation %{processed_jobs: 0, success: 0, failure: 0}
def start_link(_) do
GenServer.start_link(__MODULE__, @initial_state, name: __MODULE__)
end
@impl true
def init(state) do
:telemetry.attach("oban-monitor-failure", [:oban, :failure], &handle_event/4, nil)
:telemetry.attach("oban-monitor-success", [:oban, :success], &handle_event/4, nil)
{:ok, state}
end
def stats do
GenServer.call(__MODULE__, :stats)
end
def handle_event([:oban, status], %{duration: duration}, meta, _) do
GenServer.cast(__MODULE__, {:process_event, status, duration, meta})
end
@impl true
def handle_call(:stats, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast({:process_event, status, duration, meta}, state) do
state =
state
|> Map.update!(:workers, fn workers ->
workers
|> Map.put_new(meta.worker, %{})
|> Map.update!(meta.worker, &update_worker(&1, status, meta, duration))
end)
|> Map.update!(:queues, fn workers ->
workers
|> Map.put_new(meta.queue, @queue)
|> Map.update!(meta.queue, &update_queue(&1, status, meta, duration))
end)
|> Map.update!(:processed_jobs, &(&1 + 1))
{:noreply, state}
end
defp update_worker(worker, status, meta, duration) do
worker
|> Map.put_new(meta.args["op"], @operation)
|> Map.update!(meta.args["op"], &update_op(&1, status, meta, duration))
end
defp update_op(op, :enqueue, _meta, _duration) do
op
|> Map.update!(:enqueued, &(&1 + 1))
end
defp update_op(op, status, _meta, _duration) do
op
|> Map.update!(:processed_jobs, &(&1 + 1))
|> Map.update!(status, &(&1 + 1))
end
defp update_queue(queue, status, _meta, _duration) do
queue
|> Map.update!(:processed_jobs, &(&1 + 1))
|> Map.update!(status, &(&1 + 1))
end
end

View File

@ -15,6 +15,7 @@ defmodule Pleroma.ReverseProxy do
@valid_resp_codes [200, 206, 304] @valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30) @max_read_duration :timer.seconds(30)
@max_body_length :infinity @max_body_length :infinity
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD) @methods ~w(GET HEAD)
@moduledoc """ @moduledoc """
@ -48,6 +49,8 @@ defmodule Pleroma.ReverseProxy do
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to * `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
read from the remote upstream. read from the remote upstream.
* `failed_request_ttl` (default `#{inspect(@failed_request_ttl)}` ms): the time the failed request is cached and cannot be retried.
* `inline_content_types`: * `inline_content_types`:
* `true` will not alter `content-disposition` (up to the upstream), * `true` will not alter `content-disposition` (up to the upstream),
* `false` will add `content-disposition: attachment` to any request, * `false` will add `content-disposition: attachment` to any request,
@ -83,6 +86,7 @@ defmodule Pleroma.ReverseProxy do
{:keep_user_agent, boolean} {:keep_user_agent, boolean}
| {:max_read_duration, :timer.time() | :infinity} | {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity} | {:max_body_length, non_neg_integer() | :infinity}
| {:failed_request_ttl, :timer.time() | :infinity}
| {:http, []} | {:http, []}
| {:req_headers, [{String.t(), String.t()}]} | {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]} | {:resp_headers, [{String.t(), String.t()}]}
@ -108,7 +112,8 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
opts opts
end end
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts), with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
{:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
:ok <- :ok <-
header_length_constraint( header_length_constraint(
headers, headers,
@ -116,12 +121,18 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
) do ) do
response(conn, client, url, code, headers, opts) response(conn, client, url, code, headers, opts)
else else
{:ok, true} ->
conn
|> error_or_redirect(url, 500, "Request failed", opts)
|> halt()
{:ok, code, headers} -> {:ok, code, headers} ->
head_response(conn, url, code, headers, opts) head_response(conn, url, code, headers, opts)
|> halt() |> halt()
{:error, {:invalid_http_response, code}} -> {:error, {:invalid_http_response, code}} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}") Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
track_failed_url(url, code, opts)
conn conn
|> error_or_redirect( |> error_or_redirect(
@ -134,6 +145,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
{:error, error} -> {:error, error} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}") Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
track_failed_url(url, error, opts)
conn conn
|> error_or_redirect(url, 500, "Request failed", opts) |> error_or_redirect(url, 500, "Request failed", opts)
@ -388,4 +400,17 @@ defp increase_read_duration(_) do
end end
defp client, do: Pleroma.ReverseProxy.Client defp client, do: Pleroma.ReverseProxy.Client
defp track_failed_url(url, code, opts) do
code = to_string(code)
ttl =
if code in ["403", "404"] or String.starts_with?(code, "5") do
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
else
nil
end
Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
end
end end

View File

@ -11,6 +11,7 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Ecto.Multi alias Ecto.Multi
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
@ -583,7 +584,7 @@ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id) get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false -> restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id) get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) -> restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
@ -842,6 +843,61 @@ def maybe_update_following_count(%User{local: false} = user) do
def maybe_update_following_count(user), do: user def maybe_update_following_count(user), do: user
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [
info:
fragment(
"jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
u.info,
p.count
)
]
)
|> where([u], u.id == ^user.id)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def set_unread_conversation_count(_), do: :noop
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
Participation.unread_conversation_count_for_user(user)
|> where([p], p.conversation_id == ^conversation.id)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [
info:
fragment(
"jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> where([u], u.id == ^user.id)
|> where([u, p], p.count == 0)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def increment_unread_conversation_count(_, _), do: :noop
def remove_duplicated_following(%User{following: following} = user) do def remove_duplicated_following(%User{following: following} = user) do
uniq_following = Enum.uniq(following) uniq_following = Enum.uniq(following)

View File

@ -47,6 +47,7 @@ defmodule Pleroma.User.Info do
field(:hide_followers, :boolean, default: false) field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true) field(:hide_favorites, :boolean, default: true)
field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: []) field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false}) field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil) field(:mascot, :map, default: nil)

View File

@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -291,8 +292,8 @@ def reject(%{to: to, actor: actor, object: object} = params) do
end end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
activity_id = params[:activity_id]
with data <- %{ with data <- %{
"to" => to, "to" => to,
@ -301,6 +302,7 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
"actor" => actor, "actor" => actor,
"object" => object "object" => object
}, },
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
@ -346,7 +348,7 @@ def announce(
local \\ true, local \\ true,
public \\ true public \\ true
) do ) do
with true <- is_public?(object), with true <- is_announceable?(object, user, public),
announce_data <- make_announce_data(user, object, activity_id, public), announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object), {:ok, object} <- add_announce_to_object(activity, object),

View File

@ -82,38 +82,6 @@ def track_object_fetch(conn, object_id) do
conn conn
end end
def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
else
{:public?, false} ->
{:error, :not_found}
end
end
def object_likes(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes})
else
{:public?, false} ->
{:error, :not_found}
end
end
def activity(conn, %{"uuid" => uuid}) do def activity(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid), with ap_id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.normalize(ap_id), %Activity{} = activity <- Activity.normalize(ap_id),

View File

@ -580,7 +580,7 @@ def handle_incoming(
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_embedded_obj_helper(object_id, actor),
public <- Visibility.is_public?(data), public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity} {:ok, activity}
@ -621,7 +621,8 @@ def handle_incoming(
to: data["to"] || [], to: data["to"] || [],
cc: data["cc"] || [], cc: data["cc"] || [],
object: object, object: object,
actor: actor_id actor: actor_id,
activity_id: data["id"]
}) })
else else
e -> e ->
@ -753,6 +754,24 @@ def handle_incoming(
end end
end end
# For Undos that don't have the complete object attached, try to find it in our database.
def handle_incoming(
%{
"type" => "Undo",
"object" => object
} = activity,
options
)
when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity
|> Map.put("object", data)
|> handle_incoming(options)
else
_e -> :error
end
end
def handle_incoming(_, _), do: :error def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
@ -763,6 +782,29 @@ def get_obj_helper(id, options \\ []) do
end end
end end
@spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
ap_id: ap_id
})
when attributed_to == ap_id do
with {:ok, activity} <-
handle_incoming(%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => attributed_to,
"object" => data
}) do
{:ok, Object.normalize(activity)}
else
_ -> get_obj_helper(object_id)
end
end
def get_embedded_obj_helper(object_id, _) do
get_obj_helper(object_id)
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(in_reply_to, "http"), with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
@ -812,6 +854,27 @@ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
|> Object.normalize()
data =
if Visibility.is_private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
end
data =
data
|> strip_internal_fields
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does. # because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do def prepare_outgoing(%{"type" => "Accept"} = data) do

View File

@ -251,16 +251,6 @@ def get_existing_like(actor, %{data: %{"id" => id}}) do
|> Repo.one() |> Repo.one()
end end
@doc """
Returns like activities targeting an object
"""
def get_object_likes(%{data: %{"id" => id}}) do
id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Like")
|> Repo.all()
end
@spec make_like_data(User.t(), map(), String.t()) :: map() @spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data( def make_like_data(
%User{ap_id: ap_id} = actor, %User{ap_id: ap_id} = actor,
@ -461,14 +451,16 @@ def make_announce_data(
""" """
def make_unannounce_data( def make_unannounce_data(
%User{ap_id: ap_id} = user, %User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id activity_id
) do ) do
object = Object.normalize(object)
%{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]], "to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
@ -477,14 +469,16 @@ def make_unannounce_data(
def make_unlike_data( def make_unlike_data(
%User{ap_id: ap_id} = user, %User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id activity_id
) do ) do
object = Object.normalize(object)
%{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]], "to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
@ -494,7 +488,7 @@ def make_unlike_data(
@spec add_announce_to_object(Activity.t(), Object.t()) :: @spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object( def add_announce_to_object(
%Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}}, %Activity{data: %{"actor" => actor}},
object object
) do ) do
announcements = take_announcements(object) announcements = take_announcements(object)
@ -745,6 +739,6 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> Repo.all() |> Repo.all()
end end
defp maybe_put(map, _key, nil), do: map def maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value) def maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View File

@ -37,40 +37,4 @@ def render("object.json", %{object: %Activity{} = activity}) do
Map.merge(base, additional) Map.merge(base, additional)
end end
def render("likes.json", %{ap_id: ap_id, likes: likes, page: page}) do
collection(likes, "#{ap_id}/likes", page)
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end
def render("likes.json", %{ap_id: ap_id, likes: likes}) do
%{
"id" => "#{ap_id}/likes",
"type" => "OrderedCollection",
"totalItems" => length(likes),
"first" => collection(likes, "#{ap_id}/likes", 1)
}
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end
def collection(collection, iri, page) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn object -> Transmogrifier.prepare_object(object.data) end)
total = length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
"orderedItems" => items
}
if offset + length(items) < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
end end

View File

@ -22,7 +22,7 @@ def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user})
def render("endpoints.json", %{user: %User{local: true} = _user}) do def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{ %{
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize), "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app), "oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange), "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox), "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media) "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)

View File

@ -27,6 +27,11 @@ def is_private?(activity) do
end end
end end
def is_announceable?(activity, user, public \\ true) do
is_public?(activity) ||
(!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
end
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true def is_direct?(%Object{data: %{"directMessage" => true}}), do: true

View File

@ -16,6 +16,8 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -76,11 +78,12 @@ def delete(activity_id, user) do
end end
end end
def repeat(id_or_ap_id, user) do def repeat(id_or_ap_id, user, params \\ %{}) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity), object <- Object.normalize(activity),
nil <- Utils.get_existing_announce(user.ap_id, object) do nil <- Utils.get_existing_announce(user.ap_id, object),
ActivityPub.announce(user, object) public <- public_announce?(object, params) do
ActivityPub.announce(user, object, nil, true, public)
else else
_ -> {:error, dgettext("errors", "Could not repeat")} _ -> {:error, dgettext("errors", "Could not repeat")}
end end
@ -169,6 +172,14 @@ defp normalize_and_validate_choices(choices, object) do
end end
end end
def public_announce?(_, %{"visibility" => visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
def public_announce?(object, _) do
Visibility.is_public?(object)
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{"visibility" => visibility}, in_reply_to, _) def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
@ -262,7 +273,7 @@ def update(user) do
ActivityPub.update(%{ ActivityPub.update(%{
local: true, local: true,
to: [user.follower_address], to: [Pleroma.Constants.as_public(), user.follower_address],
cc: [], cc: [],
actor: user.ap_id, actor: user.ap_id,
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})

View File

@ -75,4 +75,16 @@ def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end end
end end
def try_render(conn, target, params)
when is_binary(target) do
case render(conn, target, params) do
nil -> render_error(conn, :not_implemented, "Can't display this activity")
res -> res
end
end
def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity")
end
end end

View File

@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
alias Pleroma.User
@doc "GET /web/*path"
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
if user && token do
conn
|> put_layout(false)
|> render("index.html", token: token, user: user, custom_emojis: Pleroma.Emoji.get_all())
else
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
@doc "PUT /api/web/settings"
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
json(conn, %{})
else
e ->
conn
|> put_status(:internal_server_error)
|> json(%{error: inspect(e)})
end
end
end

View File

@ -105,6 +105,17 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup() |> Enum.dedup()
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
info_params = info_params =
[ [
:no_rich_text, :no_rich_text,
@ -122,12 +133,12 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end) end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "fields", :fields, fn fields -> |> add_if_present(params, "fields_attributes", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields} {:ok, fields}
end) end)
|> add_if_present(params, "fields", :raw_fields) |> add_if_present(params, "fields_attributes", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)} {:ok, Map.merge(user.info.pleroma_settings_store, value)}
end) end)
@ -168,6 +179,14 @@ defp add_if_present(map, params, params_field, map_field, value_function \\ &{:o
end end
end end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
fields
end
end
@doc "GET /api/v1/accounts/relationships" @doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
targets = User.get_all_by_ids(List.wrap(id)) targets = User.get_all_by_ids(List.wrap(id))
@ -301,4 +320,26 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
end end
end end
@doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
render(conn, "show.json", user: followed, for: follower)
else
{:followed, _} -> {:error, :not_found}
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
end
end end

View File

@ -0,0 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppController do
use Pleroma.Web, :controller
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@local_mastodon_name "Mastodon-Local"
@doc "POST /api/v1/apps"
def create(conn, params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
|> Map.drop(["scope", "scopes"])
|> Map.put("scopes", scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
{:ok, app} <- Repo.insert(cs) do
render(conn, "show.json", app: app)
end
end
@doc "GET /api/v1/apps/verify_credentials"
def verify_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
render(conn, "short.json", app: app)
end
end
end

View File

@ -0,0 +1,91 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@local_mastodon_name "Mastodon-Local"
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
@doc "Local Mastodon FE login init action"
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: local_mastodon_root_path(conn))
end
end
@doc "Local Mastodon FE callback action"
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: Enum.join(app.scopes, " ")
)
redirect(conn, to: path)
end
end
@doc "DELETE /auth/sign_out"
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
@doc "POST /auth/password"
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
conn
|> put_status(:no_content)
|> json("")
else
{:error, "unknown user"} ->
send_resp(conn, :not_found, "")
{:error, _} ->
send_resp(conn, :bad_request, "")
end
end
defp local_mastodon_root_path(conn) do
case get_session(conn, :return_to) do
nil ->
masto_fe_path(conn, :index, ["getting-started"])
return_to ->
delete_session(conn, :return_to)
return_to
end
end
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do
%{client_name: @local_mastodon_name, redirect_uris: "."}
|> App.get_or_make(["read", "write", "follow", "push"])
end
end

View File

@ -0,0 +1,11 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end
end

View File

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
end
@doc "GET /api/v1/instance/peers"
def peers(conn, _params) do
json(conn, Pleroma.Stats.get_peers())
end
end

View File

@ -5,437 +5,10 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
require Logger require Logger
plug(RateLimiter, :password_reset when action == :password_reset)
@local_mastodon_name "Mastodon-Local"
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def create_app(conn, params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
|> Map.drop(["scope", "scopes"])
|> Map.put("scopes", scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
{:ok, app} <- Repo.insert(cs) do
conn
|> put_view(AppView)
|> render("show.json", %{app: app})
end
end
def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
conn
|> put_view(AppView)
|> render("short.json", %{app: app})
end
end
@mastodon_api_level "2.7.2"
def masto_instance(conn, _params) do
instance = Config.get(:instance)
response = %{
uri: Web.base_url(),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
languages: ["en"],
registrations: Pleroma.Config.get([:instance, :registrations_open]),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits)
}
json(conn, response)
end
def peers(conn, _params) do
json(conn, Stats.get_peers())
end
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url,
"tags" => tags,
# Assuming that a comma is authorized in the category name
"category" => (tags -- ["Custom"]) |> Enum.join(",")
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json(conn, mastodon_emoji)
end
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
error when is_nil(error) or error == false ->
render_error(conn, :not_found, "Record not found")
end
end
defp get_cached_vote_or_vote(user, object, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
{_, res} =
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
case CommonAPI.vote(user, object, choices) do
{:error, _message} = res -> {:ignore, res}
res -> {:commit, res}
end
end)
res
end
def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
with %Object{} = object <- Object.get_by_id(id),
true <- object.data["type"] == "Question",
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
conn
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
nil ->
render_error(conn, :not_found, "Record not found")
false ->
render_error(conn, :not_found, "Record not found")
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end
def update_media(
%{assigns: %{user: user}} = conn,
%{"id" => id, "description" => description} = _
)
when is_binary(description) do
with %Object{} = object <- Repo.get(Object, id),
true <- Object.authorize_mutation(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
attachment_data = Map.put(data, "id", object.id)
conn
|> put_view(StatusView)
|> render("attachment.json", %{attachment: attachment_data})
end
end
def update_media(_conn, _data), do: {:error, :bad_request}
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
attachment_data = Map.put(object.data, "id", object.id)
conn
|> put_view(StatusView)
|> render("attachment.json", %{attachment: attachment_data})
end
end
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}
{:error, message} ->
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_accounts <- User.blocked_users(user) do
res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
json(conn, res)
end
end
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
limit = Config.get([:instance, :limit])
accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
initial_state =
%{
meta: %{
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
mascot: User.get_mascot(user)["url"]
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
},
compose: %{
me: "#{user.id}",
default_privacy: user.info.default_scope,
default_sensitive: false,
allow_content_types: Config.get([:instance, :allowed_post_formats])
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings:
user.info.settings ||
%{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: limit
}
|> Jason.encode!()
conn
|> put_layout(false)
|> put_view(MastodonView)
|> render("index.html", %{initial_state: initial_state})
else
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
json(conn, %{})
else
e ->
conn
|> put_status(:internal_server_error)
|> json(%{error: inspect(e)})
end
end
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
@doc "Local Mastodon FE login init action"
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: local_mastodon_root_path(conn))
end
end
@doc "Local Mastodon FE callback action"
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: Enum.join(app.scopes, " ")
)
redirect(conn, to: path)
end
end
defp local_mastodon_root_path(conn) do
case get_session(conn, :return_to) do
nil ->
mastodon_api_path(conn, :index, ["getting-started"])
return_to ->
delete_session(conn, :return_to)
return_to
end
end
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do
App.get_or_make(
%{client_name: @local_mastodon_name, redirect_uris: "."},
["read", "write", "follow", "push"]
)
end
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
# Stubs for unimplemented mastodon api # Stubs for unimplemented mastodon api
# #
def empty_array(conn, _) do def empty_array(conn, _) do
@ -447,83 +20,4 @@ def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object") Logger.debug("Unimplemented, returning an empty object")
json(conn, %{}) json(conn, %{})
end end
def suggestions(%{assigns: %{user: user}} = conn, _) do
suggestions = Config.get(:suggestions)
if Keyword.get(suggestions, :enabled, false) do
api = Keyword.get(suggestions, :third_party_engine, "")
timeout = Keyword.get(suggestions, :timeout, 5000)
limit = Keyword.get(suggestions, :limit, 23)
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname
url =
api
|> String.replace("{{host}}", host)
|> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
{:ok, data} <- Jason.decode(body) do
data =
data
|> Enum.slice(0, limit)
|> Enum.map(fn x ->
x
|> Map.put("id", fetch_suggestion_id(x))
|> Map.put("avatar", MediaProxy.url(x["avatar"]))
|> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
end)
json(conn, data)
else
e ->
Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
else
json(conn, [])
end
end
defp fetch_suggestion_id(attrs) do
case User.get_or_fetch(attrs["acct"]) do
{:ok, %User{id: id}} -> id
_ -> 0
end
end
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
conn
|> put_status(:no_content)
|> json("")
else
{:error, "unknown user"} ->
send_resp(conn, :not_found, "")
{:error, _} ->
send_resp(conn, :bad_request, "")
end
end
def try_render(conn, target, params)
when is_binary(target) do
case render(conn, target, params) do
nil -> render_error(conn, :not_implemented, "Can't display this activity")
res -> res
end
end
def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity")
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
end end

View File

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MediaController do
use Pleroma.Web, :controller
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
@doc "POST /api/v1/media"
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
attachment_data = Map.put(object.data, "id", object.id)
render(conn, "attachment.json", %{attachment: attachment_data})
end
end
@doc "PUT /api/v1/media/:id"
def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description})
when is_binary(description) do
with %Object{} = object <- Object.get_by_id(id),
true <- Object.authorize_mutation(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
attachment_data = Map.put(data, "id", object.id)
render(conn, "attachment.json", %{attachment: attachment_data})
end
end
def update(_conn, _data), do: {:error, :bad_request}
end

View File

@ -0,0 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [try_render: 3, json_response: 3]
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/polls/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json", %{object: object, for: user})
else
error when is_nil(error) or error == false ->
render_error(conn, :not_found, "Record not found")
end
end
@doc "POST /api/v1/polls/:id/votes"
def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
try_render(conn, "show.json", %{object: object, for: user})
else
nil -> render_error(conn, :not_found, "Record not found")
false -> render_error(conn, :not_found, "Record not found")
{:error, message} -> json_response(conn, :unprocessable_entity, %{error: message})
end
end
defp get_cached_vote_or_vote(user, object, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
Cachex.fetch!(:idempotency_cache, idempotency_key, fn ->
case CommonAPI.vote(user, object, choices) do
{:error, _message} = res -> {:ignore, res}
res -> {:commit, res}
end
end)
end
end

View File

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3] import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
require Ecto.Query require Ecto.Query
@ -125,8 +125,8 @@ def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
@doc "POST /api/v1/statuses/:id/reblog" @doc "POST /api/v1/statuses/:id/reblog"
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do %Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end end
@ -242,7 +242,19 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
Object.normalize(activity) do
announces =
"Announce"
|> Activity.Queries.by_type()
|> Ecto.Query.where([a], a.actor in ^announces)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.all()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
|> Enum.map(& &1.actor)
|> Enum.uniq()
users = users =
User User
|> Ecto.Query.where([u], u.ap_id in ^announces) |> Ecto.Query.where([u], u.ap_id in ^announces)
@ -271,4 +283,39 @@ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
render(conn, "context.json", activity: activity, activities: activities, user: user) render(conn, "context.json", activity: activity, activities: activities, user: user)
end end
end end
@doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end
@doc "GET /api/v1/bookmarks"
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
bookmarks =
user.id
|> Bookmark.for_user_query()
|> Pleroma.Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end end

View File

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.MediaProxy
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/suggestions"
def index(%{assigns: %{user: user}} = conn, _) do
if Config.get([:suggestions, :enabled], false) do
with {:ok, data} <- fetch_suggestions(user) do
limit = Config.get([:suggestions, :limit], 23)
data =
data
|> Enum.slice(0, limit)
|> Enum.map(fn x ->
x
|> Map.put("id", fetch_suggestion_id(x))
|> Map.put("avatar", MediaProxy.url(x["avatar"]))
|> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
end)
json(conn, data)
end
else
json(conn, [])
end
end
defp fetch_suggestions(user) do
api = Config.get([:suggestions, :third_party_engine], "")
timeout = Config.get([:suggestions, :timeout], 5000)
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
url =
api
|> String.replace("{{host}}", host)
|> String.replace("{{user}}", user.nickname)
with {:ok, %{status: 200, body: body}} <-
Pleroma.HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]) do
Jason.decode(body)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
end
defp fetch_suggestion_id(attrs) do
case User.get_or_fetch(attrs["acct"]) do
{:ok, %User{id: id}} -> id
_ -> 0
end
end
end

View File

@ -167,6 +167,7 @@ defp do_render("show.json", %{user: user} = opts) do
|> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for]) |> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
end end
defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(string) when is_binary(string) do
@ -248,6 +249,16 @@ defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
defp maybe_put_activation_status(data, _, _), do: data defp maybe_put_activation_status(data, _, _), do: data
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
user.info.unread_conversation_count
)
end
defp maybe_put_unread_conversation_count(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View File

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do
use Pleroma.Web, :view
alias Pleroma.Emoji
alias Pleroma.Web
def render("index.json", %{custom_emojis: custom_emojis}) do
render_many(custom_emojis, __MODULE__, "show.json")
end
def render("show.json", %{custom_emoji: {shortcode, %Emoji{file: relative_url, tags: tags}}}) do
url = Web.base_url() |> URI.merge(relative_url) |> to_string()
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url,
"tags" => tags,
# Assuming that a comma is authorized in the category name
"category" => tags |> List.delete("Custom") |> Enum.join(",")
}
end
end

View File

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
@mastodon_api_level "2.7.2"
def render("show.json", _) do
instance = Pleroma.Config.get(:instance)
%{
uri: Pleroma.Web.base_url(),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg",
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits),
upload_limit: Keyword.get(instance, :upload_limit),
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
}
end
end

View File

@ -1,8 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonView do
use Pleroma.Web, :view
import Phoenix.HTML
end

View File

@ -25,40 +25,44 @@ def render("show.json", %{
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity) mastodon_type = Activity.mastodon_notification_type(activity)
response = %{ with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
id: to_string(notification.id), response = %{
type: mastodon_type, id: to_string(notification.id),
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), type: mastodon_type,
account: AccountView.render("show.json", %{user: actor, for: user}), created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
pleroma: %{ account: account,
is_seen: notification.seen pleroma: %{
is_seen: notification.seen
}
} }
}
case mastodon_type do case mastodon_type do
"mention" -> "mention" ->
response response
|> Map.merge(%{ |> Map.merge(%{
status: StatusView.render("show.json", %{activity: activity, for: user}) status: StatusView.render("show.json", %{activity: activity, for: user})
}) })
"favourite" -> "favourite" ->
response response
|> Map.merge(%{ |> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user}) status: StatusView.render("show.json", %{activity: parent_activity, for: user})
}) })
"reblog" -> "reblog" ->
response response
|> Map.merge(%{ |> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user}) status: StatusView.render("show.json", %{activity: parent_activity, for: user})
}) })
"follow" -> "follow" ->
response response
_ -> _ ->
nil nil
end
else
_ -> nil
end end
end end
end end

View File

@ -0,0 +1,74 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollView do
use Pleroma.Web, :view
alias Pleroma.HTML
alias Pleroma.Web.CommonAPI.Utils
def render("show.json", %{object: object, multiple: multiple, options: options} = params) do
{end_time, expired} = end_time_and_expired(object)
{options, votes_count} = options_and_votes_count(options)
%{
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
expires_at: end_time,
expired: expired,
multiple: multiple,
votes_count: votes_count,
options: options,
voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
}
end
def render("show.json", %{object: object} = params) do
case object.data do
%{"anyOf" => options} when is_list(options) ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
%{"oneOf" => options} when is_list(options) ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
_ ->
nil
end
end
defp end_time_and_expired(object) do
case object.data["closed"] || object.data["endTime"] do
end_time when is_binary(end_time) ->
end_time = NaiveDateTime.from_iso8601!(end_time)
expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
{Utils.to_masto_date(end_time), expired}
_ ->
{nil, false}
end
end
defp options_and_votes_count(options) do
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
current_count = option["replies"]["totalItems"] || 0
{%{
title: HTML.strip_tags(name),
votes_count: current_count
}, current_count + count}
end)
end
defp voted?(%{object: object} = opts) do
if opts[:for] do
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
else
false
end
end
end

View File

@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@ -124,7 +125,7 @@ def render(
pinned: pinned?(activity, user), pinned: pinned?(activity, user),
sensitive: false, sensitive: false,
spoiler_text: "", spoiler_text: "",
visibility: "public", visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [], media_attachments: reblogged[:media_attachments] || [],
mentions: mentions, mentions: mentions,
tags: reblogged[:tags] || [], tags: reblogged[:tags] || [],
@ -277,7 +278,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
spoiler_text: summary_html, spoiler_text: summary_html,
visibility: get_visibility(object), visibility: get_visibility(object),
media_attachments: attachments, media_attachments: attachments,
poll: render("poll.json", %{object: object, for: opts[:for]}), poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions, mentions: mentions,
tags: build_tags(tags), tags: build_tags(tags),
application: %{ application: %{
@ -389,75 +390,6 @@ def render("listens.json", opts) do
safe_render_many(opts.activities, StatusView, "listen.json", opts) safe_render_many(opts.activities, StatusView, "listen.json", opts)
end end
def render("poll.json", %{object: object} = opts) do
{multiple, options} =
case object.data do
%{"anyOf" => options} when is_list(options) -> {true, options}
%{"oneOf" => options} when is_list(options) -> {false, options}
_ -> {nil, nil}
end
if options do
{end_time, expired} =
case object.data["closed"] || object.data["endTime"] do
end_time when is_binary(end_time) ->
end_time =
(object.data["closed"] || object.data["endTime"])
|> NaiveDateTime.from_iso8601!()
expired =
end_time
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|> case do
:lt -> true
_ -> false
end
end_time = Utils.to_masto_date(end_time)
{end_time, expired}
_ ->
{nil, false}
end
voted =
if opts[:for] do
existing_votes =
Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
else
false
end
{options, votes_count} =
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
current_count = option["replies"]["totalItems"] || 0
{%{
title: HTML.strip_tags(name),
votes_count: current_count
}, current_count + count}
end)
%{
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
expires_at: end_time,
expired: expired,
multiple: multiple,
votes_count: votes_count,
options: options,
voted: voted,
emojis: build_emojis(object.data["emoji"])
}
else
nil
end
end
def render("context.json", %{activity: activity, activities: activities, user: user}) do def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} = %{ancestors: ancestors, descendants: descendants} =
activities activities

View File

@ -213,13 +213,31 @@ def token_exchange(
{:auth_active, false} -> {:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/ # Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address") render_error(
conn,
:forbidden,
"Your login is missing a confirmed e-mail address",
%{},
"missing_confirmed_email"
)
{:user_active, false} -> {:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled") render_error(
conn,
:forbidden,
"Your account is currently disabled",
%{},
"account_is_disabled"
)
{:password_reset_pending, true} -> {:password_reset_pending, true} ->
render_error(conn, :forbidden, "Password reset is required") render_error(
conn,
:forbidden,
"Password reset is required",
%{},
"password_reset_required"
)
_error -> _error ->
render_invalid_credentials_error(conn) render_invalid_credentials_error(conn)
@ -443,7 +461,7 @@ defp do_create_authorization(
end end
# Special case: Local MastodonFE # Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login) defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri

View File

@ -347,14 +347,14 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", FollowRequestController, :index) get("/follow_requests", FollowRequestController, :index)
get("/blocks", MastodonAPIController, :blocks) get("/blocks", AccountController, :blocks)
get("/mutes", MastodonAPIController, :mutes) get("/mutes", AccountController, :mutes)
get("/timelines/home", TimelineController, :home) get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct) get("/timelines/direct", TimelineController, :direct)
get("/favourites", MastodonAPIController, :favourites) get("/favourites", StatusController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks) get("/bookmarks", StatusController, :bookmarks)
get("/notifications", NotificationController, :index) get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show) get("/notifications/:id", NotificationController, :show)
@ -373,7 +373,7 @@ defmodule Pleroma.Web.Router do
get("/filters", FilterController, :index) get("/filters", FilterController, :index)
get("/suggestions", MastodonAPIController, :suggestions) get("/suggestions", SuggestionController, :index)
get("/conversations", ConversationController, :index) get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :read) post("/conversations/:id/read", ConversationController, :read)
@ -403,10 +403,10 @@ defmodule Pleroma.Web.Router do
put("/scheduled_statuses/:id", ScheduledActivityController, :update) put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
post("/polls/:id/votes", MastodonAPIController, :poll_vote) post("/polls/:id/votes", PollController, :vote)
post("/media", MastodonAPIController, :upload) post("/media", MediaController, :create)
put("/media/:id", MastodonAPIController, :update_media) put("/media/:id", MediaController, :update)
delete("/lists/:id", ListController, :delete) delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create) post("/lists", ListController, :create)
@ -426,7 +426,7 @@ defmodule Pleroma.Web.Router do
scope [] do scope [] do
pipe_through(:oauth_follow) pipe_through(:oauth_follow)
post("/follows", MastodonAPIController, :follows) post("/follows", AccountController, :follows)
post("/accounts/:id/follow", AccountController, :follow) post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow) post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block) post("/accounts/:id/block", AccountController, :block)
@ -451,31 +451,31 @@ defmodule Pleroma.Web.Router do
end end
end end
scope "/api/web", Pleroma.Web.MastodonAPI do scope "/api/web", Pleroma.Web do
pipe_through([:authenticated_api, :oauth_write]) pipe_through([:authenticated_api, :oauth_write])
put("/settings", MastodonAPIController, :put_settings) put("/settings", MastoFEController, :put_settings)
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api) pipe_through(:api)
post("/accounts", AccountController, :create) post("/accounts", AccountController, :create)
get("/accounts/search", SearchController, :account_search)
get("/instance", MastodonAPIController, :masto_instance) get("/instance", InstanceController, :show)
get("/instance/peers", MastodonAPIController, :peers) get("/instance/peers", InstanceController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials) post("/apps", AppController, :create)
get("/custom_emojis", MastodonAPIController, :custom_emojis) get("/apps/verify_credentials", AppController, :verify_credentials)
get("/statuses/:id/card", StatusController, :card) get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by) get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/trends", MastodonAPIController, :empty_array) get("/custom_emojis", CustomEmojiController, :index)
get("/accounts/search", SearchController, :account_search) get("/trends", MastodonAPIController, :empty_array)
scope [] do scope [] do
pipe_through(:oauth_read_or_public) pipe_through(:oauth_read_or_public)
@ -488,7 +488,7 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id", StatusController, :show) get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context) get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", MastodonAPIController, :get_poll) get("/polls/:id", PollController, :show)
get("/accounts/:id/statuses", AccountController, :statuses) get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers) get("/accounts/:id/followers", AccountController, :followers)
@ -580,7 +580,6 @@ defmodule Pleroma.Web.Router do
pipe_through(:ostatus) pipe_through(:ostatus)
get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/users/:nickname/outbox", ActivityPubController, :outbox)
get("/objects/:uuid/likes", ActivityPubController, :object_likes)
end end
pipeline :activitypub_client do pipeline :activitypub_client do
@ -658,17 +657,17 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end end
scope "/", Pleroma.Web.MastodonAPI do scope "/", Pleroma.Web do
pipe_through(:mastodon_html) pipe_through(:mastodon_html)
get("/web/login", MastodonAPIController, :login) get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPIController, :logout) delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
post("/auth/password", MastodonAPIController, :password_reset) post("/auth/password", MastodonAPI.AuthController, :password_reset)
scope [] do scope [] do
pipe_through(:oauth_read) pipe_through(:oauth_read)
get("/web/*path", MastodonAPIController, :index) get("/web/*path", MastoFEController, :index)
end end
end end

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.Ping do defmodule Pleroma.Web.Streamer.Ping do
use GenServer use GenServer
require Logger require Logger

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.State do defmodule Pleroma.Web.Streamer.State do
use GenServer use GenServer
require Logger require Logger

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.StreamerSocket do defmodule Pleroma.Web.Streamer.StreamerSocket do
defstruct transport_pid: nil, user: nil defstruct transport_pid: nil, user: nil

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.Supervisor do defmodule Pleroma.Web.Streamer.Supervisor do
use Supervisor use Supervisor

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.Worker do defmodule Pleroma.Web.Streamer.Worker do
use GenServer use GenServer
@ -128,11 +132,14 @@ defp should_send?(%User{} = user, %Activity{} = item) do
blocks = user.info.blocks || [] blocks = user.info.blocks || []
mutes = user.info.mutes || [] mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || [] reblog_mutes = user.info.muted_reblogs || []
recipient_blocks = MapSet.new(blocks ++ mutes)
recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
with parent when not is_nil(parent) <- Object.normalize(item), with parent when not is_nil(parent) <- Object.normalize(item),
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor), %{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]), %{host: parent_host} <- URI.parse(parent.data["actor"]),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
@ -194,11 +201,8 @@ def push_to_socket(topics, topic, item) do
# Get the current user so we have up-to-date blocks etc. # Get the current user so we have up-to-date blocks etc.
if socket_user do if socket_user do
user = User.get_cached_by_ap_id(socket_user.ap_id) user = User.get_cached_by_ap_id(socket_user.ap_id)
blocks = user.info.blocks || []
mutes = user.info.mutes || []
with true <- Enum.all?([blocks, mutes], &(item.actor not in &1)), if should_send?(user, item) do
true <- thread_containment(item, user) do
send(transport_pid, {:text, StreamerView.render("update.json", item, user)}) send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
end end
else else

View File

@ -14,7 +14,7 @@
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'> <link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'> <link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'> <link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>
<script id='initial-state' type='application/json'><%= raw @initial_state %></script> <script id='initial-state' type='application/json'><%= initial_state(@token, @user, @custom_emojis) %></script>
<script src="/packs/core/common.js"></script> <script src="/packs/core/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/core/common.css" /> <link rel="stylesheet" media="all" href="/packs/core/common.css" />

View File

@ -3,15 +3,27 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TranslationHelpers do defmodule Pleroma.Web.TranslationHelpers do
defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do defmacro render_error(
conn,
status,
msgid,
bindings \\ Macro.escape(%{}),
identifier \\ Macro.escape("")
) do
quote do quote do
require Pleroma.Web.Gettext require Pleroma.Web.Gettext
error_map =
%{
error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)),
identifier: unquote(identifier)
}
|> Enum.reject(fn {_k, v} -> v == "" end)
|> Map.new()
unquote(conn) unquote(conn)
|> Plug.Conn.put_status(unquote(status)) |> Plug.Conn.put_status(unquote(status))
|> Phoenix.Controller.json(%{ |> Phoenix.Controller.json(error_map)
error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings))
})
end end
end end
end end

View File

@ -0,0 +1,102 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEView do
use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.CustomEmojiView
@default_settings %{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
}
def initial_state(token, user, custom_emojis) do
limit = Config.get([:instance, :limit])
%{
meta: %{
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
mascot: User.get_mascot(user)["url"]
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
},
compose: %{
me: "#{user.id}",
default_privacy: user.info.default_scope,
default_sensitive: false,
allow_content_types: Config.get([:instance, :allowed_post_formats])
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings: user.info.settings || @default_settings,
push_subscription: nil,
accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis),
char_limit: limit
}
|> Jason.encode!()
|> Phoenix.HTML.raw()
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
end

View File

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddUnreadConversationCountToUserInfo do
use Ecto.Migration
def up do
execute("""
update users set info = jsonb_set(info, '{unread_conversation_count}', 0::varchar::jsonb, true) where local=true
""")
end
def down, do: :ok
end

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