Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
845 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66b32e237..e5167b4f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,687 +1,688 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [unreleased]
### Changed
<details>
<summary>API Changes</summary>
- **Breaking:** Emoji API: changed methods and renamed routes.
</details>
### Removed
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
<details>
<summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps.
+- Admin API: endpoint for status view.
</details>
### Fixed
- Support pagination in conversations API
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
## [unreleased-patch]
### Security
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
### Fixed
- Logger configuration through AdminFE
- HTTP Basic Authentication permissions issue
- ObjectAgePolicy didn't filter out old messages
### Added
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
## [2.0.2] - 2020-04-08
### Added
- Support for Funkwhale's `Audio` activity
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/update_credentials`
### Fixed
- Blocked/muted users still generating push notifications
- Input textbox for bio ignoring newlines
- OTP: Inability to use PostgreSQL databases with SSL
- `user delete_activities` breaking when trying to delete already deleted posts
- Incorrect URL for Funkwhale channels
### Upgrade notes
1. Restart Pleroma
## [2.0.1] - 2020-03-15
### Security
- Static-FE: Fix remote posts not being sanitized
### Fixed
- 500 errors when no `Accept` header is present if Static-FE is enabled
- Instance panel not being updated immediately due to wrong `Cache-Control` headers
- Statuses posted with BBCode/Markdown having unncessary newlines in Pleroma-FE
- OTP: Fix some settings not being migrated to in-database config properly
- No `Cache-Control` headers on attachment/media proxy requests
- Character limit enforcement being off by 1
- Mastodon Streaming API: hashtag timelines not working
### Changed
- BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
- Mastodon API: Allow registration without email if email verification is not enabled
### Upgrade notes
#### Nginx only
1. Remove `proxy_ignore_headers Cache-Control;` and `proxy_hide_header Cache-Control;` from your config.
#### Everyone
1. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
2. Restart Pleroma
## [2.0.0] - 2019-03-08
### Security
- Mastodon API: Fix being able to request enormous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
- **Breaking**: OStatus protocol support
- **Breaking**: MDII uploader
- **Breaking**: Using third party engines for user recommendation
<details>
<summary>API Changes</summary>
- **Breaking**: AdminAPI: migrate_from_db endpoint
</details>
### Changed
- **Breaking:** Pleroma won't start if it detects unapplied migrations
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- **Breaking:** `Pleroma.Plugs.RemoteIp` and `:rate_limiter` enabled by default. Please ensure your reverse proxy forwards the real IP!
- **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default
- **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features.
- **Breaking:** Dynamic configuration has been rearchitected. The `:pleroma, :instance, dynamic_configuration` setting has been replaced with `config :pleroma, configurable_from_database`. Please backup your configuration to a file and run the migration task to ensure consistency with the new schema.
- **Breaking:** `:instance, no_attachment_links` has been replaced with `:instance, attachment_links` which still takes a boolean value but doesn't use double negative language.
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Enabled `:instance, extended_nickname_format` in the default config
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
- Extract RSS functionality from OStatus
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
- OStatus: Extract RSS functionality
- Deprecated `User.Info` embedded schema (fields moved to `User`)
- Store status data inside Flag activity
- Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
- Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
- Logger: default log level changed from `warn` to `info`.
- Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
- Allow account registration without an email
- Default to `prepare: :unnamed` in the database configuration.
- Instance stats are now loaded on startup instead of being empty until next hourly job.
<details>
<summary>API Changes</summary>
- **Breaking** EmojiReactions: Change endpoints and responses to align with Mastodon
- **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body)
- **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- **Breaking** replying to reports is now "report notes", endpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes`
- Mastodon API: stopped sanitizing display names, field names and subject fields since they are supposed to be treated as plaintext
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
- Admin API: Support authentication via `x-admin-token` HTTP header
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
- Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity
- Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
- Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
</details>
### Added
- `:chat_limit` option to limit chat characters.
- `cleanup_attachments` option to remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
- Refreshing poll results for remote polls
- Authentication: Added rate limit for password-authorized actions / login existence checks
- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mix task to list all users (`mix pleroma.user list`)
- Mix task to send a test email (`mix pleroma.email test`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
- User notification settings: Add `privacy_option` option.
- Support for custom Elixir modules (such as MRF policies)
- User settings: Add _This account is a_ option.
- A new users admin digest email
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
- Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
- ActivityPub: support for `replies` collection (output for outgoing federation & fetching on incoming federation).
- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
<details>
<summary>API Changes</summary>
- Job queue stats to the healthcheck page
- Admin API: Add ability to fetch reports, grouped by status `GET /api/pleroma/admin/grouped_reports`
- Admin API: Add ability to require password reset
- 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: `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
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
- Metadata Link: Atom syndication Feed
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
- Admin API: Multiple endpoints now require `nicknames` array, instead of singe `nickname`:
- `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group`
- `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body)
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
- ActivityPub: Support `Move` activities
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
- Configuration: `feed` option for user atom feed.
- Pleroma API: Add Emoji reactions
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
- Admin API: Add `/api/pleroma/admin/users/:nickname/statuses` - lists all statuses from a given user
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
- ActivityPub: Configurable `type` field of the actors.
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
- Captcha: Support native provider
- Captcha: Enable by default
- Mastodon API: Add support for `account_id` param to filter notifications by the account
- Mastodon API: Add `emoji_reactions` property to Statuses
- Mastodon API: Change emoji reaction reply format
- Notifications: Added `pleroma:emoji_reaction` notification type
- Mastodon API: Change emoji reaction reply format once more
- Configuration: `feed.logo` option for tag feed.
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
- Mastodon API: Add `reacted` property to `emoji_reactions`
- Pleroma API: Add reactions for a single emoji.
- ActivityPub: `[:activitypub, :note_replies_output_limit]` setting sets the number of note self-replies to output on outgoing federation.
- Admin API: `GET /api/pleroma/admin/stats` to get status count by visibility scope
- Admin API: `GET /api/pleroma/admin/statuses` - list all statuses (accepts `godmode` and `local_only`)
</details>
### Fixed
- Report emails now include functional links to profiles of remote user accounts
- Not being able to log in to some third-party apps when logged in to MastoFE
- MRF: `Delete` activities being exempt from MRF policies
- OTP releases: Not being able to configure OAuth expired token cleanup interval
- OTP releases: Not being able to configure HTML sanitization policy
- OTP releases: Not being able to change upload limit (again)
- Favorites timeline now ordered by favorite date instead of post date
- Support for cancellation of a follow request
<details>
<summary>API Changes</summary>
- 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`
- AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports)
- Admin API: Error when trying to update reports in the "old" format
- Mastodon API: Marking a conversation as read (`POST /api/v1/conversations/:id/read`) now no longer brings it to the top in the user's direct conversation list
</details>
## [1.1.9] - 2020-02-10
### Fixed
- OTP: Inability to set the upload limit (again)
- Not being able to pin polls
- Streaming API: incorrect handling of reblog mutes
- Rejecting the user when field length limit is exceeded
- OpenGraph provider: html entities in descriptions
## [1.1.8] - 2020-01-10
### Fixed
- Captcha generation issues
- Returned Kocaptcha endpoint to configuration
- Captcha validity is now 5 minutes
## [1.1.7] - 2019-12-13
### Fixed
- OTP: Inability to set the upload limit
- OTP: Inability to override node name/distribution type to run 2 Pleroma instances on the same machine
### Added
- Integrated captcha provider
### Changed
- Captcha enabled by default
- Default Captcha provider changed from `Pleroma.Captcha.Kocaptcha` to `Pleroma.Captcha.Native`
- Better `Cache-Control` header for static content
### Bundled Pleroma-FE Changes
#### Added
- Icons in the navigation panel
#### Fixed
- Improved support unauthenticated view of private instances
#### Removed
- Whitespace hack on empty post content
## [1.1.6] - 2019-11-19
### Fixed
- Not being able to log into to third party apps when the browser is logged into mastofe
- Email confirmation not being required even when enabled
- Mastodon API: conversations API crashing when one status is malformed
### Bundled Pleroma-FE Changes
#### Added
- About page
- Meme arrows
#### Fixed
- Image modal not closing unless clicked outside of image
- Attachment upload spinner not being centered
- Showing follow counters being 0 when they are actually hidden
## [1.1.5] - 2019-11-09
### Fixed
- Polls having different numbers in timelines/notifications/poll api endpoints due to cache desyncronization
- Pleroma API: OAuth token endpoint not being found when ".json" suffix is appended
### Changed
- Frontend bundle updated to [044c9ad0](https://git.pleroma.social/pleroma/pleroma-fe/commit/044c9ad0562af059dd961d50961a3880fca9c642)
## [1.1.4] - 2019-11-01
### Fixed
- Added a migration that fills up empty user.info fields to prevent breakage after previous unsafe migrations.
- Failure to migrate from pre-1.0.0 versions
- Mastodon API: Notification stream not including follow notifications
## [1.1.3] - 2019-10-25
### Fixed
- Blocked users showing up in notifications collapsed as if they were muted
- `pleroma_ctl` not working on Debian's default shell
## [1.1.2] - 2019-10-18
### Fixed
- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.
## [1.1.1] - 2019-10-18
### Fixed
- One of the migrations between 1.0.0 and 1.1.0 wiping user info of the relay user because of unexpected behavior of postgresql's `jsonb_set`, resulting in inability to post in the default configuration. If you were affected, please run the following query in postgres console, the relay user will be recreated automatically:
```
delete from users where ap_id = 'https://your.instance.hostname/relay';
```
- Bad user search matches
## [1.1.0] - 2019-10-14
**Breaking:** The stable branch has been changed from `master` to `stable`. If you want to keep using 1.0, the `release/1.0` branch will receive security updates for 6 months after 1.1 release.
**OTP Note:** `pleroma_ctl` in 1.0 defaults to `master` and doesn't support specifying arbitrary branches, making `./pleroma_ctl update` fail. To fix this, fetch a version of `pleroma_ctl` from 1.1 using the command below and proceed with the update normally:
```
curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/develop/rel/files/bin/pleroma_ctl'
```
### Security
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
### Removed
- **Breaking:** GNU Social API with Qvitter extensions support
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
- ActivityPub: The `/objects/:uuid/likes` endpoint.
### Changed
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
- Configuration: added `config/description.exs`, from which `docs/config.md` is generated
- Configuration: OpenGraph and TwitterCard providers enabled by default
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
- NodeInfo: Return `mailerEnabled` in `metadata`
- Mastodon API: Unsubscribe followers when they unfollow a user
- Mastodon API: `pleroma.thread_muted` key in the Status entity
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
- The `Pleroma.FlakeId` module has been replaced with the `flake_id` library.
### Fixed
- Following from Osada
- Favorites timeline doing database-intensive queries
- Metadata rendering errors resulting in the entire page being inaccessible
- `federation_incoming_replies_max_depth` option being ignored in certain cases
- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
- Mastodon API: Misskey's endless polls being unable to render
- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
- Mastodon API: Notifications endpoint crashing if one notification failed to render
- Mastodon API: `exclude_replies` is correctly handled again.
- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
- 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: Ensure the `account` field is not empty when rendering Notification entities.
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
- Rich Media: Parser failing when no TTL can be found by image TTL setters
- Rich Media: The crawled URL is now spliced into the rich media data.
- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
- ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
- ActivityPub: Correct addressing of Undo.
- ActivityPub: Correct addressing of profile update activities.
- ActivityPub: Polls are now refreshed when necessary.
- Report emails now include functional links to profiles of remote user accounts
- Existing user id not being preserved on insert conflict
- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
- Report email not being sent to admins when the reporter is a remote user
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
### Added
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour.
- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
- MRF: Support for excluding specific domains from Transparency.
- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
- Mastodon API, extension: Ability to reset avatar, profile banner, and background
- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
- Mastodon API: Add support for muting/unmuting notifications
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
- Mastodon API: Add support for the `domain_blocking` attribute in the relationship API (`GET /api/v1/accounts/relationships`).
- Mastodon API: Add `pleroma.deactivated` to the Account entity
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
- Mastodon API: Improve support for the user profile custom fields
- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID
- Admin API: Added support for `tuples`.
- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db
- Added synchronization of following/followers counters for external users
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
- Configuration: `user_bio_length` and `user_name_length` options.
- Addressable lists
- Twitter API: added rate limit for `/api/account/password_reset` endpoint.
- ActivityPub: Add an internal service actor for fetching ActivityPub objects.
- ActivityPub: Optional signing of ActivityPub object fetches.
- Admin API: Endpoint for fetching latest user's statuses
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
- Pleroma API: Email change endpoint.
- Admin API: Added moderation log
- Web response cache (currently, enabled for ActivityPub)
- Reverse Proxy: Do not retry failed requests to limit pressure on the peer
### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Admin API: changed json structure for saving config settings.
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.
## [1.0.7] - 2019-09-26
### Fixed
- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
### Changed
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
## [1.0.6] - 2019-08-14
### Fixed
- MRF: fix use of unserializable keyword lists in describe() implementations
- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
## [1.0.5] - 2019-08-13
### Fixed
- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
- Mastodon API: `muted` in the Status entity, using author's account to determine if the thread was muted
- Mastodon API: return the actual profile URL in the Account entity's `url` property when appropriate
- Templates: properly style anchor tags
- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
- Not being able to access the Mastodon FE login page on private instances
- MRF: ensure that subdomain_match calls are case-insensitive
- Fix internal server error when using the healthcheck API.
### Added
- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
- Relays: Added a task to list relay subscriptions.
- MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
- MRF (Simple Policy): Support for wildcard domains.
- Support for wildcard domains in user domain blocks setting.
- Configuration: `quarantined_instances` support wildcard domains.
- Mix Tasks: `mix pleroma.database fix_likes_collections`
- Configuration: `federation_incoming_replies_max_depth` option
### Removed
- Federation: Remove `likes` from objects.
- **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
## [1.0.4] - 2019-08-01
### Fixed
- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
## [1.0.3] - 2019-07-31
### Security
- OStatus: eliminate the possibility of a protocol downgrade attack.
- OStatus: prevent following locked accounts, bypassing the approval process.
- TwitterAPI: use CommonAPI to handle remote follows instead of OStatus.
## [1.0.2] - 2019-07-28
### Fixed
- Not being able to pin unlisted posts
- Mastodon API: represent poll IDs as strings
- MediaProxy: fix matching filenames
- MediaProxy: fix filename encoding
- Migrations: fix a sporadic migration failure
- Metadata rendering errors resulting in the entire page being inaccessible
- Federation/MediaProxy not working with instances that have wrong certificate order
- ActivityPub S2S: remote user deletions now work the same as local user deletions.
### Changed
- Configuration: OpenGraph and TwitterCard providers enabled by default
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
## [1.0.1] - 2019-07-14
### Security
- OStatus: fix an object spoofing vulnerability.
## [1.0.0] - 2019-06-29
### Security
- Mastodon API: Fix display names not being sanitized
- Rich media: Do not crawl private IP ranges
### Added
- Digest email for inactive users
- Add a generic settings store for frontends / clients to use.
- Explicit addressing option for posting.
- Optional SSH access mode. (Needs `erlang-ssh` package on some distributions).
- [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
- LDAP authentication
- External OAuth provider authentication
- Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html)
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database bump_all_conversations`
- Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Mix Tasks: `mix pleroma.config migrate_to_db`
- Mix Tasks: `mix pleroma.config migrate_from_db`
- Federation: Support for `Question` and `Answer` objects
- Federation: Support for reports
- Configuration: `poll_limits` option
- Configuration: `pack_extensions` option
- Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option
- Configuration: `report_uri` option
- Configuration: `email_notifications` option
- Configuration: `limit_to_local_content` option
- Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint
- Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints
- Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other
- Admin API: added filters (role, tags, email, name) for users endpoint
- Admin API: Endpoints for managing reports
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses
- Admin API: Endpoints to view and change config settings.
- AdminFE: initial release with basic user management accessible at /pleroma/admin/
- Mastodon API: Add chat token to `verify_credentials` response
- Mastodon API: Add background image setting to `update_credentials`
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- Mastodon API: `POST /api/v1/accounts` (account creation API)
- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
- ActivityPub C2S: OAuth endpoints
- Metadata: RelMe provider
- OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager
- Object pruning (`mix pleroma.database prune_objects`)
- OAuth: added job to clean expired access tokens
- MRF: Support for rejecting reports from specific instances (`mrf_simple`)
- MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
- MRF: Support for running subchains.
- Configuration: `skip_thread_containment` option
- Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
- MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.
- Configuration: `ignore_hosts` option
- Configuration: `ignore_tld` option
- Configuration: default syslog tag "Pleroma" is now lowercased to "pleroma"
### Changed
- **Breaking:** bind to 127.0.0.1 instead of 0.0.0.0 by default
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
- Thread containment / test for complete visibility will be skipped by default.
- Enforcement of OAuth scopes
- Add multiple use/time expiring invite token
- Restyled OAuth pages to fit with Pleroma's default theme
- Link/mention/hashtag detection is now handled by [auto_linker](https://git.pleroma.social/pleroma/auto_linker)
- NodeInfo: Return `safe_dm_mentions` feature flag
- Federation: Expand the audience of delete activities to all recipients of the deleted object
- Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default
- Configuration: Default log level in `prod` environment is now set to `warn`
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Admin API: Move the user related API to `api/pleroma/admin/users`
- Admin API: `POST /api/pleroma/admin/users` will take list of users
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity
- Mastodon API: Add `pleroma.conversation_id`, `pleroma.in_reply_to_account_acct` fields to the Status entity
- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending`, `pleroma.hide_followers`, `pleroma.hide_follows`, `pleroma.hide_favorites` fields to the User entity
- Mastodon API: Add `pleroma.show_role`, `pleroma.no_rich_text` fields to the Source subentity
- Mastodon API: Add support for updating `no_rich_text`, `hide_followers`, `hide_follows`, `hide_favorites`, `show_role` in `PATCH /api/v1/update_credentials`
- Mastodon API: Add `pleroma.is_seen` to the Notification entity
- Mastodon API: Add `pleroma.local` to the Status entity
- Mastodon API: Add `preview` parameter to `POST /api/v1/statuses`
- Mastodon API: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
- Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack
- Hide deactivated users and their statuses
- Posts which are marked sensitive or tagged nsfw no longer have link previews.
- HTTP connection timeout is now set to 10 seconds.
- Respond with a 404 Not implemented JSON error message when requested API is not implemented
- Rich Media: crawl only https URLs.
### Fixed
- Follow requests don't get 'stuck' anymore.
- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
- Followers counter not being updated when a follower is blocked
- Deactivated users being able to request an access token
- Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak
- Proper Twitter Card generation instead of a dummy
- Deletions failing for users with a large number of posts
- NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox
- Federation: Handling of objects without `summary` property
- Federation: Add a language tag to activities as required by ActivityStreams 2.0
- Federation: Do not federate avatar/banner if set to default allowing other servers/clients to use their defaults
- Federation: Cope with missing or explicitly nulled address lists
- Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection
- Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics
- Federation: Follow requests from remote users who have been blocked will be automatically rejected if appropriate
- MediaProxy: Parse name from content disposition headers even for non-whitelisted types
- MediaProxy: S3 link encoding
- Rich Media: Reject any data which cannot be explicitly encoded into JSON
- Pleroma API: Importing follows from Mastodon 2.8+
- Twitter API: Exposing default scope, `no_rich_text` of the user to anyone
- Twitter API: Returning the `role` object in user entity despite `show_role = false`
- Mastodon API: `/api/v1/favourites` serving only public activities
- Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies
- Mastodon API: Streaming API broadcasting wrong activity id
- Mastodon API: 500 errors when requesting a card for a private conversation
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone
- Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]
- Mastodon API: Replace missing non-nullable Card attributes with empty strings
- User-Agent is now sent correctly for all HTTP requests.
- MRF: Simple policy now properly delists imported or relayed statuses
## Removed
- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`
## [0.9.99999] - 2019-05-31
### Security
- Mastodon API: Fix lists leaking private posts
## [0.9.9999] - 2019-04-05
### Security
- Mastodon API: Fix content warnings skipping HTML sanitization
## [0.9.999] - 2019-03-13
Frontend changes only.
### Added
- Added floating action button for posting status on mobile
### Changed
- Changed user-settings icon to a pencil
### Fixed
- Keyboard shortcuts activating when typing a message
- Gaps when scrolling down on a timeline after showing new
## [0.9.99] - 2019-03-08
### Changed
- Update the frontend to the 0.9.99 tag
### Fixed
- Sign the date header in federation to fix Mastodon federation.
## [0.9.9] - 2019-02-22
This is our first stable release.
diff --git a/config/config.exs b/config/config.exs
index a6c6d6f99..ca9bbab64 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,658 +1,660 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil
config :pleroma, Pleroma.Captcha,
enabled: true,
seconds_valid: 300,
method: Pleroma.Captcha.Native
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: false,
proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com"
config :pleroma, :emoji,
shortcode_globs: ["/emoji/custom/**/*.png"],
pack_extensions: [".png", ".gif"],
groups: [
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1},
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]
# Configures Elixir's Logger
config :logger, :console,
level: :debug,
format: "\n$time $metadata[$level] $message\n",
metadata: [:request_id]
config :logger, :ex_syslogger,
level: :debug,
ident: "pleroma",
format: "$metadata[$level] $message",
metadata: [:request_id]
config :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
config :tesla, adapter: Tesla.Adapter.Gun
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
send_user_agent: true,
user_agent: :default,
adapter: []
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
invites_enabled: false,
account_activation_required: false,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher
],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
mrf_transparency: true,
mrf_transparency_exclusions: [],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: true,
cleanup_attachments: false
config :pleroma, :extensions, output_relationships_in_statuses_by_default: true
config :pleroma, :feed,
post_title: %{
max_length: 100,
omission: "..."
}
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
allow_inline_images: true,
allow_headings: false,
allow_tables: false,
allow_fonts: false,
scrub_policy: [
Pleroma.HTML.Scrubber.Default,
Pleroma.HTML.Transform.MediaProxy
]
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true
},
masto_fe: %{
showInstanceSpecificPanel: true
}
config :pleroma, :assets,
mascots: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
],
default_mascot: :pleroma_fox_tan
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.png",
type: "image/png"
}
],
theme_color: "#282c37",
background_color: "#191b22"
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: true,
follow_handshake_timeout: 500,
note_replies_output_limit: 5,
sign_object_fetches: true,
authorized_fetch_mode: false
config :pleroma, :streamer,
workers: 3,
overflow_workers: 2
config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_normalize_markup, scrub_policy: Pleroma.HTML.Scrubber.Default
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
accept: [],
avatar_removal: [],
banner_removal: [],
reject_deletes: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_vocabulary,
accept: [],
reject: []
config :pleroma, :mrf_object_age,
threshold: 172_800,
actions: [:delist, :strip_followers]
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"],
parsers: [
Pleroma.Web.RichMedia.Parsers.TwitterCard,
Pleroma.Web.RichMedia.Parsers.OGP,
Pleroma.Web.RichMedia.Parsers.OEmbed
],
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
config :pleroma, :media_proxy,
enabled: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
whitelist: []
config :pleroma, :chat, enabled: true
config :phoenix, :format_encoders, json: Jason
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "confirm"]
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, Pleroma.Web.Metadata,
providers: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.Feed
],
unfurl_nsfw: false
config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin"
config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web"
]
config :pleroma, Oban,
repo: Pleroma.Repo,
verbose: false,
prune: {:maxlen, 1500},
queues: [
activity_expiration: 10,
federator_incoming: 50,
federator_outgoing: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
background: 5,
remote_fetcher: 2,
attachments_cleanup: 5,
new_users_digest: 1
],
crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
{"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
]
config :auto_linker,
opts: [
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost",
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
config :esshd,
enabled: false
oauth_consumer_strategies =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(&hd(String.split(&1, ":")))
ueberauth_providers =
for strategy <- oauth_consumer_strategies do
strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}"
strategy_module = String.to_atom(strategy_module_name)
{String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}}
end
config :ueberauth,
Ueberauth,
base_path: "/oauth",
providers: ueberauth_providers
config :pleroma,
:auth,
enforce_oauth_admin_scope_usage: true,
oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
config :pleroma, Pleroma.Emails.UserEmail,
logo: nil,
styling: %{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false
config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 300,
enabled: true
config :pleroma, :email_notifications,
digest: %{
active: false,
interval: 7,
inactivity_threshold: 7
}
config :pleroma, :oauth2,
token_expires_in: 600,
issue_new_refresh_token: true,
clean_expired_tokens: false
config :pleroma, :database, rum_enabled: false
config :pleroma, :env, Mix.env()
config :http_signatures,
adapter: Pleroma.Signature
config :pleroma, :rate_limit,
authentication: {60_000, 15},
timeline: {500, 3},
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25},
relations_actions: {10_000, 10},
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},
status_id_action: {60_000, 3},
password_reset: {1_800_000, 5},
account_confirmation_resend: {8_640_000, 5},
ap_routes: {60_000, 15}
config :pleroma, Pleroma.ActivityExpiration, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true
config :pleroma, :static_fe, enabled: false
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
config :pleroma, :modules, runtime_dir: "instance/modules"
config :pleroma, configurable_from_database: false
config :pleroma, Pleroma.Repo,
parameters: [gin_fuzzy_search_limit: "500"],
prepare: :unnamed
config :pleroma, :connections_pool,
checkin_timeout: 250,
max_connections: 250,
retry: 1,
retry_timeout: 1000,
await_up_timeout: 5_000
config :pleroma, :pools,
federation: [
size: 50,
max_overflow: 10,
timeout: 150_000
],
media: [
size: 50,
max_overflow: 10,
timeout: 150_000
],
upload: [
size: 25,
max_overflow: 5,
timeout: 300_000
],
default: [
size: 10,
max_overflow: 2,
timeout: 10_000
]
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
config :pleroma, :restrict_unauthenticated,
timelines: %{local: false, federated: false},
profiles: %{local: false, remote: false},
activities: %{local: false, remote: false}
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index 9d8e3b93c..1b2afebef 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1,3198 +1,3213 @@
use Mix.Config
alias Pleroma.Docs.Generator
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
config :pleroma, :config_description, [
%{
group: :pleroma,
key: Pleroma.Upload,
type: :group,
description: "Upload general settings",
children: [
%{
key: :uploader,
type: :module,
description: "Module which will be used for uploads",
suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.S3]
},
%{
key: :filters,
type: {:list, :module},
description: "List of filter modules for uploads",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/upload/filter",
"Elixir.Pleroma.Upload.Filter."
)
},
%{
key: :link_name,
type: :boolean,
description:
"If enabled, a name parameter will be added to the url of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`."
},
%{
key: :base_url,
type: :string,
description: "Base url for the uploads, needed if you use CDN",
suggestions: [
"https://cdn-host.com"
]
},
%{
key: :proxy_remote,
type: :boolean,
description:
"If enabled, requests to media stored using a remote uploader will be proxied instead of being redirected"
},
%{
key: :proxy_opts,
type: :keyword,
description: "Options for Pleroma.ReverseProxy",
suggestions: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
children: [
%{
key: :redirect_on_failure,
type: :boolean,
description:
"Redirects the client to the real remote URL if there's any HTTP errors. " <>
"Any error during body processing will not be redirected as the response is chunked."
},
%{
key: :max_body_length,
type: :integer,
description:
"Limits the content length to be approximately the " <>
"specified length. It is validated with the `content-length` header and also verified when proxying."
},
%{
key: :http,
type: :keyword,
description: "HTTP options",
children: [
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS versions to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
}
]
}
]
},
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
}
]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.Local,
type: :group,
description: "Local uploader-related settings",
children: [
%{
key: :uploads,
type: :string,
description: "Path where user's uploads will be saved",
suggestions: [
"uploads"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.S3,
type: :group,
description: "S3 uploader-related settings",
children: [
%{
key: :bucket,
type: :string,
description: "S3 bucket",
suggestions: [
"bucket"
]
},
%{
key: :bucket_namespace,
type: :string,
description: "S3 bucket namespace",
suggestions: ["pleroma"]
},
%{
key: :public_endpoint,
type: :string,
description: "S3 endpoint",
suggestions: ["https://s3.amazonaws.com"]
},
%{
key: :truncated_namespace,
type: :string,
description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
" For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint."
},
%{
key: :streaming_enabled,
type: :boolean,
description:
"Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.Mogrify,
type: :group,
description: "Uploads mogrify filter settings",
children: [
%{
key: :args,
type: [:string, {:list, :string}, {:list, :tuple}],
description: "List of actions for the mogrify command",
suggestions: [
"strip",
"auto-orient",
{"implode", "1"}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.AnonymizeFilename,
type: :group,
description: "Filter replaces the filename of the upload",
children: [
%{
key: :text,
type: :string,
description:
"Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original" <>
" filename extension by using {extension}, for example custom-file-name.{extension}.",
suggestions: [
"custom-file-name.{extension}"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.Mailer,
type: :group,
description: "Mailer-related settings",
children: [
%{
key: :adapter,
type: :module,
description:
"One of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters)," <>
" or Swoosh.Adapters.Local for in-memory mailbox",
suggestions: [
Swoosh.Adapters.SMTP,
Swoosh.Adapters.Sendgrid,
Swoosh.Adapters.Sendmail,
Swoosh.Adapters.Mandrill,
Swoosh.Adapters.Mailgun,
Swoosh.Adapters.Mailjet,
Swoosh.Adapters.Postmark,
Swoosh.Adapters.SparkPost,
Swoosh.Adapters.AmazonSES,
Swoosh.Adapters.Dyn,
Swoosh.Adapters.SocketLabs,
Swoosh.Adapters.Gmail,
Swoosh.Adapters.Local
]
},
%{
key: :enabled,
type: :boolean,
description: "Allow/disallow send emails"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :relay,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["smtp.gmail.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :username,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["pleroma"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :password,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["password"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :ssl,
label: "SSL",
type: :boolean,
description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :tls,
label: "TLS",
type: :atom,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [:always, :never, :if_available]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :auth,
type: :atom,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [:always, :never, :if_available]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :port,
type: :integer,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [1025]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :retries,
type: :integer,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [5]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :no_mx_lookups,
label: "No MX lookups",
type: :boolean,
description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Sendgrid},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Sendgrid` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_path,
type: :string,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting",
suggestions: ["/usr/bin/sendmail"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_args,
type: :string,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting",
suggestions: ["-N delay,failure,success"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :qmail,
type: :boolean,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Mandrill},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Mandrill` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Mailgun` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :domain,
type: :string,
description: "`Swoosh.Adapters.Mailgun` adapter specific setting",
suggestions: ["pleroma.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Mailjet` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :secret,
type: :string,
description: "`Swoosh.Adapters.Mailjet` adapter specific setting",
suggestions: ["my-secret-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Postmark},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Postmark` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.SparkPost` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :endpoint,
type: :string,
description: "`Swoosh.Adapters.SparkPost` adapter specific setting",
suggestions: ["https://api.sparkpost.com/api/v1"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :region,
type: :string,
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["us-east-1", "us-east-2"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :access_key,
type: :string,
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["aws-access-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :secret,
type: :string,
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["aws-secret-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Dyn},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.Dyn` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :server_id,
type: :string,
description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :api_key,
label: "API key",
type: :string,
description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Gmail},
key: :access_token,
type: :string,
description: "`Swoosh.Adapters.Gmail` adapter specific setting"
}
]
},
%{
group: :swoosh,
type: :group,
description: "`Swoosh.Adapters.Local` adapter specific settings",
children: [
%{
group: {:subgroup, Swoosh.Adapters.Local},
key: :serve_mailbox,
type: :boolean,
description: "Run the preview server together as part of your app"
},
%{
group: {:subgroup, Swoosh.Adapters.Local},
key: :preview_port,
type: :integer,
description: "The preview server port",
suggestions: [4001]
}
]
},
%{
group: :pleroma,
key: :uri_schemes,
type: :group,
description: "URI schemes related settings",
children: [
%{
key: :valid_schemes,
type: {:list, :string},
description: "List of the scheme part that is considered valid to be an URL",
suggestions: [
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
}
]
},
%{
group: :pleroma,
key: :instance,
type: :group,
description: "Instance-related settings",
children: [
%{
key: :name,
type: :string,
description: "Name of the instance",
suggestions: [
"Pleroma"
]
},
%{
key: :email,
label: "Admin Email Address",
type: :string,
description: "Email used to reach an Administrator/Moderator of the instance",
suggestions: [
"email@example.com"
]
},
%{
key: :notify_email,
label: "Sender Email Address",
type: :string,
description: "Envelope FROM address for mail sent via Pleroma",
suggestions: [
"notify@example.com"
]
},
%{
key: :description,
type: :string,
description:
"The instance's description. It can be seen in nodeinfo and `/api/v1/instance`",
suggestions: [
"Very cool instance"
]
},
%{
key: :limit,
type: :integer,
description: "Posts character limit (CW/Subject included in the counter)",
suggestions: [
5_000
]
},
%{
key: :chat_limit,
type: :integer,
description: "Character limit of the instance chat messages",
suggestions: [
5_000
]
},
%{
key: :remote_limit,
type: :integer,
description: "Hard character limit beyond which remote posts will be dropped",
suggestions: [
100_000
]
},
%{
key: :upload_limit,
type: :integer,
description: "File size limit of uploads (except for avatar, background, banner)",
suggestions: [
16_000_000
]
},
%{
key: :avatar_upload_limit,
type: :integer,
description: "File size limit of user's profile avatars",
suggestions: [
2_000_000
]
},
%{
key: :background_upload_limit,
type: :integer,
description: "File size limit of user's profile backgrounds",
suggestions: [
4_000_000
]
},
%{
key: :banner_upload_limit,
type: :integer,
description: "File size limit of user's profile banners",
suggestions: [
4_000_000
]
},
%{
key: :poll_limits,
type: :map,
description: "A map with poll limits for local polls",
suggestions: [
%{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 31_536_000
}
],
children: [
%{
key: :max_options,
type: :integer,
description: "Maximum number of options",
suggestions: [20]
},
%{
key: :max_option_chars,
type: :integer,
description: "Maximum number of characters per option",
suggestions: [200]
},
%{
key: :min_expiration,
type: :integer,
description: "Minimum expiration time (in seconds)",
suggestions: [0]
},
%{
key: :max_expiration,
type: :integer,
description: "Maximum expiration time (in seconds)",
suggestions: [3600]
}
]
},
%{
key: :registrations_open,
type: :boolean,
description:
"Enable registrations for anyone. Invitations require this setting to be disabled."
},
%{
key: :invites_enabled,
type: :boolean,
description:
"Enable user invitations for admins (depends on `registrations_open` being disabled)."
},
%{
key: :account_activation_required,
type: :boolean,
description: "Require users to confirm their emails before signing in."
},
%{
key: :federating,
type: :boolean,
description: "Enable federation with other instances."
},
%{
key: :federation_incoming_replies_max_depth,
label: "Fed. incoming replies max depth",
type: :integer,
description:
"Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while" <>
" fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
suggestions: [
100
]
},
%{
key: :federation_reachability_timeout_days,
label: "Fed. reachability timeout days",
type: :integer,
description:
"Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
suggestions: [
7
]
},
%{
key: :federation_publisher_modules,
type: {:list, :module},
description: "List of modules for federation publishing",
suggestions: [
Pleroma.Web.ActivityPub.Publisher
]
},
%{
key: :allow_relay,
type: :boolean,
description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance"
},
%{
key: :rewrite_policy,
type: [:module, {:list, :module}],
description: "A list of MRF policies enabled",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/web/activity_pub/mrf",
"Elixir.Pleroma.Web.ActivityPub.MRF."
)
},
%{
key: :public,
type: :boolean,
description:
"Makes the client API in authentificated mode-only except for user-profiles." <>
" Useful for disabling the Local Timeline and The Whole Known Network."
},
%{
key: :quarantined_instances,
type: {:list, :string},
description:
"List of ActivityPub instances where private (DMs, followers-only) activities will not be send",
suggestions: [
"quarantined.com",
"*.quarantined.com"
]
},
%{
key: :managed_config,
type: :boolean,
description:
"Whenether the config for pleroma-fe is configured in this config or in static/config.json"
},
%{
key: :static_dir,
type: :string,
description: "Instance static directory",
suggestions: [
"instance/static/"
]
},
%{
key: :allowed_post_formats,
type: {:list, :string},
description: "MIME-type list of formats allowed to be posted (transformed into HTML)",
suggestions: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
]
},
%{
key: :mrf_transparency,
label: "MRF transparency",
type: :boolean,
description:
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
},
%{
key: :mrf_transparency_exclusions,
label: "MRF transparency exclusions",
type: {:list, :string},
description:
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
suggestions: [
"exclusion.com"
]
},
%{
key: :extended_nickname_format,
type: :boolean,
description:
"Enable to use extended local nicknames format (allows underscores/dashes)." <>
" This will break federation with older software for theses nicknames."
},
%{
key: :cleanup_attachments,
type: :boolean,
description: """
Enable to remove associated attachments when status is removed.
This will not affect duplicates and attachments without status.
Enabling this will increase load to database when deleting statuses on larger instances.
"""
},
%{
key: :max_pinned_statuses,
type: :integer,
description: "The maximum number of pinned statuses. 0 will disable the feature.",
suggestions: [
0,
1,
3
]
},
%{
key: :autofollowed_nicknames,
type: {:list, :string},
description:
"Set to nicknames of (local) users that every new user should automatically follow",
suggestions: [
"lain",
"kaniini",
"lanodan",
"rinpatch"
]
},
%{
key: :attachment_links,
type: :boolean,
description: "Enable to automatically add attachment link text to statuses"
},
%{
key: :welcome_message,
type: :string,
description:
"A message that will be sent to a newly registered users as a direct message",
suggestions: [
"Hi, @username! Welcome on board!"
]
},
%{
key: :welcome_user_nickname,
type: :string,
description: "The nickname of the local user that sends the welcome message",
suggestions: [
"lain"
]
},
%{
key: :max_report_comment_size,
type: :integer,
description: "The maximum size of the report comment. Default: 1000.",
suggestions: [
1_000
]
},
%{
key: :safe_dm_mentions,
type: :boolean,
description:
"If enabled, only mentions at the beginning of a post will be used to address people in direct messages." <>
" This is to prevent accidental mentioning of people when talking about them (e.g. \"@admin please keep an eye on @bad_actor\")." <>
" Default: disabled"
},
%{
key: :healthcheck,
type: :boolean,
description: "If enabled, system data will be shown on `/api/pleroma/healthcheck`"
},
%{
key: :remote_post_retention_days,
type: :integer,
description:
"The default amount of days to retain remote posts when pruning the database",
suggestions: [
90
]
},
%{
key: :user_bio_length,
type: :integer,
description: "A user bio maximum length. Default: 5000.",
suggestions: [
5_000
]
},
%{
key: :user_name_length,
type: :integer,
description: "A user name maximum length. Default: 100.",
suggestions: [
100
]
},
%{
key: :skip_thread_containment,
type: :boolean,
description: "Skip filtering out broken threads. Default: enabled"
},
%{
key: :limit_to_local_content,
type: {:dropdown, :atom},
description:
"Limit unauthenticated users to search for local statutes and users only. Default: `:unauthenticated`.",
suggestions: [
:unauthenticated,
:all,
false
]
},
%{
key: :max_account_fields,
type: :integer,
description: "The maximum number of custom fields in the user profile. Default: 10.",
suggestions: [
10
]
},
%{
key: :max_remote_account_fields,
type: :integer,
description:
"The maximum number of custom fields in the remote user profile. Default: 20.",
suggestions: [
20
]
},
%{
key: :account_field_name_length,
type: :integer,
description: "An account field name maximum length. Default: 512.",
suggestions: [
512
]
},
%{
key: :account_field_value_length,
type: :integer,
description: "An account field value maximum length. Default: 2048.",
suggestions: [
2048
]
},
%{
key: :external_user_synchronization,
type: :boolean,
description: "Enabling following/followers counters synchronization for external users"
}
]
},
%{
group: :logger,
type: :group,
description: "Logger-related settings",
children: [
%{
key: :backends,
type: [:atom, :tuple, :module],
description:
"Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :ex_syslogger } - to syslog, Quack.Logger - to Slack.",
suggestions: [:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]
}
]
},
%{
group: :logger,
type: :group,
key: :ex_syslogger,
description: "ExSyslogger-related settings",
children: [
%{
key: :level,
type: {:dropdown, :atom},
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :ident,
type: :string,
description:
"A string that's prepended to every message, and is typically set to the app name",
suggestions: ["pleroma"]
},
%{
key: :format,
type: :string,
description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".",
suggestions: ["$metadata[$level] $message"]
},
%{
key: :metadata,
type: {:list, :atom},
suggestions: [:request_id]
}
]
},
%{
group: :logger,
type: :group,
key: :console,
description: "Console logger settings",
children: [
%{
key: :level,
type: {:dropdown, :atom},
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :format,
type: :string,
description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".",
suggestions: ["$metadata[$level] $message"]
},
%{
key: :metadata,
type: {:list, :atom},
suggestions: [:request_id]
}
]
},
%{
group: :quack,
type: :group,
description: "Quack-related settings",
children: [
%{
key: :level,
type: {:dropdown, :atom},
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :meta,
type: {:list, :atom},
description: "Configure which metadata you want to report on",
suggestions: [
:application,
:module,
:file,
:function,
:line,
:pid,
:crash_reason,
:initial_call,
:registered_name,
:all,
:none
]
},
%{
key: :webhook_url,
type: :string,
description: "Configure the Slack incoming webhook",
suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"]
}
]
},
%{
group: :pleroma,
key: :frontend_configurations,
type: :group,
description:
"This form 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. If you want to " <>
"add your own configuration your settings all fields must be complete.",
children: [
%{
key: :pleroma_fe,
label: "Pleroma FE",
type: :map,
description: "Settings for Pleroma FE",
suggestions: [
%{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true,
logoMask: false,
logoMargin: ".1em",
stickers: false,
enableEmojiPicker: false
}
],
children: [
%{
key: :theme,
type: :string,
description: "Which theme to use, they are defined in styles.json",
suggestions: ["pleroma-dark"]
},
%{
key: :logo,
type: :string,
description: "URL of the logo, defaults to Pleroma's logo",
suggestions: ["/static/logo.png"]
},
%{
key: :background,
type: :string,
description:
"URL of the background, unless viewing a user profile with a background that is set",
suggestions: ["/images/city.jpg"]
},
%{
key: :redirectRootNoLogin,
label: "Redirect root no login",
type: :string,
description:
"Relative URL which indicates where to redirect when a user isn't logged in",
suggestions: ["/main/all"]
},
%{
key: :redirectRootLogin,
label: "Redirect root login",
type: :string,
description:
"Relative URL which indicates where to redirect when a user is logged in",
suggestions: ["/main/friends"]
},
%{
key: :showInstanceSpecificPanel,
label: "Show instance specific panel",
type: :boolean,
description: "Whenether to show the instance's specific panel"
},
%{
key: :scopeOptionsEnabled,
label: "Scope options enabled",
type: :boolean,
description: "Enable setting a notice visibility and subject/CW when posting"
},
%{
key: :formattingOptionsEnabled,
label: "Formatting options enabled",
type: :boolean,
description:
"Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to `:instance`, `allowed_post_formats`"
},
%{
key: :collapseMessageWithSubject,
label: "Collapse message with subject",
type: :boolean,
description:
"When a message has a subject (aka Content Warning), collapse it by default"
},
%{
key: :hidePostStats,
label: "Hide post stats",
type: :boolean,
description: "Hide notices statistics (repeats, favorites, ...)"
},
%{
key: :hideUserStats,
label: "Hide user stats",
type: :boolean,
description:
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
},
%{
key: :scopeCopy,
label: "Scope copy",
type: :boolean,
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
},
%{
key: :subjectLineBehavior,
label: "Subject line behavior",
type: :string,
description: "Allows changing the default behaviour of subject lines in replies.
`email`: copy and preprend re:, as in email,
`masto`: copy verbatim, as in Mastodon,
`noop`: don't copy the subject.",
suggestions: ["email", "masto", "noop"]
},
%{
key: :alwaysShowSubjectInput,
label: "Always show subject input",
type: :boolean,
description: "When disabled, auto-hide the subject field if it's empty"
},
%{
key: :logoMask,
label: "Logo mask",
type: :boolean,
description:
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
"If you want a colorful logo you must disable logoMask."
},
%{
key: :logoMargin,
label: "Logo margin",
type: :string,
description:
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
suggestions: [".1em"]
},
%{
key: :stickers,
type: :boolean,
description: "Enables stickers."
},
%{
key: :enableEmojiPicker,
label: "Emoji picker",
type: :boolean,
description: "Enables emoji picker."
}
]
},
%{
key: :masto_fe,
label: "Masto FE",
type: :map,
description: "Settings for Masto FE",
suggestions: [
%{
showInstanceSpecificPanel: true
}
],
children: [
%{
key: :showInstanceSpecificPanel,
label: "Show instance specific panel",
type: :boolean,
description: "Whenether to show the instance's specific panel"
}
]
}
]
},
%{
group: :pleroma,
key: :assets,
type: :group,
description:
"This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend",
children: [
%{
key: :mascots,
type: {:keyword, :map},
description:
"Keyword of mascots, each element must contain both an url and a mime_type key",
suggestions: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
]
},
%{
key: :default_mascot,
type: :atom,
description:
"This will be used as the default mascot on MastoFE. Default: `:pleroma_fox_tan`",
suggestions: [
:pleroma_fox_tan
]
}
]
},
%{
group: :pleroma,
key: :manifest,
type: :group,
description:
"This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE",
children: [
%{
key: :icons,
type: {:list, :map},
description: "Describe the icons of the app",
suggestion: [
%{
src: "/static/logo.png"
},
%{
src: "/static/icon.png",
type: "image/png"
},
%{
src: "/static/icon.ico",
sizes: "72x72 96x96 128x128 256x256"
}
]
},
%{
key: :theme_color,
type: :string,
description: "Describe the theme color of the app",
suggestions: ["#282c37", "mediumpurple"]
},
%{
key: :background_color,
type: :string,
description: "Describe the background color of the app",
suggestions: ["#191b22", "aliceblue"]
}
]
},
%{
group: :pleroma,
key: :mrf_simple,
label: "MRF simple",
type: :group,
description: "Message Rewrite Facility",
children: [
%{
key: :media_removal,
type: {:list, :string},
description: "List of instances to strip media attachments from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :media_nsfw,
label: "Media NSFW",
type: {:list, :string},
description: "List of instances to tag all media as NSFW (sensitive) from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :federated_timeline_removal,
type: {:list, :string},
description:
"List of instances to remove from Federated (aka The Whole Known Network) Timeline",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :reject,
type: {:list, :string},
description: "List of instances to reject activities from (except deletes)",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :accept,
type: {:list, :string},
description: "List of instances to only accept activities from (except deletes)",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :report_removal,
type: {:list, :string},
description: "List of instances to reject reports from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :avatar_removal,
type: {:list, :string},
description: "List of instances to strip avatars from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :banner_removal,
type: {:list, :string},
description: "List of instances to strip banners from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :reject_deletes,
type: {:list, :string},
description: "List of instances to reject deletions from",
suggestions: ["example.com", "*.example.com"]
}
]
},
%{
group: :pleroma,
key: :mrf_subchain,
label: "MRF subchain",
type: :group,
description:
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
" All criteria are configured as a map of regular expressions to lists of policy modules.",
children: [
%{
key: :match_actor,
type: :map,
description: "Matches a series of regular expressions against the actor field",
suggestions: [
%{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
]
}
]
},
%{
group: :pleroma,
key: :mrf_rejectnonpublic,
description:
"MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.",
label: "MRF reject non public",
type: :group,
children: [
%{
key: :allow_followersonly,
label: "Allow followers-only",
type: :boolean,
description: "Whether to allow followers-only posts"
},
%{
key: :allow_direct,
type: :boolean,
description: "Whether to allow direct messages"
}
]
},
%{
group: :pleroma,
key: :mrf_hellthread,
label: "MRF hellthread",
type: :group,
description: "Block messages with too much mentions",
children: [
%{
key: :delist_threshold,
type: :integer,
description:
"Number of mentioned users after which the message gets delisted (the message can still be seen, " <>
" but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.",
suggestions: [10]
},
%{
key: :reject_threshold,
type: :integer,
description:
"Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
suggestions: [20]
}
]
},
%{
group: :pleroma,
key: :mrf_keyword,
label: "MRF keyword",
type: :group,
description: "Reject or Word-Replace messages with a keyword or regex",
children: [
%{
key: :reject,
type: [:string, :regex],
description:
"A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :federated_timeline_removal,
type: [:string, :regex],
description:
"A list of patterns which result in message being removed from federated timelines (a.k.a unlisted). Each pattern can be a string or a regular expression.",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :replace,
type: [{:tuple, :string, :string}, {:tuple, :regex, :string}],
description:
"A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.",
suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
}
]
},
%{
group: :pleroma,
key: :mrf_mention,
label: "MRF mention",
type: :group,
description: "Block messages which mention a user",
children: [
%{
key: :actors,
type: {:list, :string},
description: "A list of actors for which any post mentioning them will be dropped.",
suggestions: ["actor1", "actor2"]
}
]
},
%{
group: :pleroma,
key: :mrf_vocabulary,
label: "MRF vocabulary",
type: :group,
description: "Filter messages which belong to certain activity vocabularies",
children: [
%{
key: :accept,
type: {:list, :string},
description:
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
},
%{
key: :reject,
type: {:list, :string},
description:
"A list of ActivityStreams terms to reject. If empty, no messages are rejected",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
}
]
},
# %{
# group: :pleroma,
# key: :mrf_user_allowlist,
# type: :group,
# description:
# "The keys in this section are the domain names that the policy should apply to." <>
# " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
# children: [
# ["example.org": ["https://example.org/users/admin"]],
# suggestions: [
# ["example.org": ["https://example.org/users/admin"]]
# ]
# ]
# },
%{
group: :pleroma,
key: :media_proxy,
type: :group,
description: "Media proxy",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables proxying of remote media to the instance's proxy"
},
%{
key: :base_url,
type: :string,
description:
"The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.",
suggestions: ["https://example.com"]
},
%{
key: :proxy_opts,
type: :keyword,
description: "Options for Pleroma.ReverseProxy",
suggestions: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
children: [
%{
key: :redirect_on_failure,
type: :boolean,
description:
"Redirects the client to the real remote URL if there's any HTTP errors. " <>
"Any error during body processing will not be redirected as the response is chunked."
},
%{
key: :max_body_length,
type: :integer,
description:
"Limits the content length to be approximately the " <>
"specified length. It is validated with the `content-length` header and also verified when proxying."
},
%{
key: :http,
type: :keyword,
description: "HTTP options",
children: [
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS version to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
}
]
}
]
},
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
}
]
}
]
},
%{
key: :whitelist,
type: {:list, :string},
description: "List of domains to bypass the mediaproxy",
suggestions: ["example.com"]
}
]
},
%{
group: :pleroma,
key: :gopher,
type: :group,
description: "Gopher settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the gopher interface"
},
%{
key: :ip,
type: :tuple,
description: "IP address to bind to",
suggestions: [{0, 0, 0, 0}]
},
%{
key: :port,
type: :integer,
description: "Port to bind to",
suggestions: [9999]
},
%{
key: :dstport,
type: :integer,
description: "Port advertised in urls (optional, defaults to port)",
suggestions: [9999]
}
]
},
%{
group: :pleroma,
key: :activitypub,
type: :group,
description: "ActivityPub-related settings",
children: [
%{
key: :unfollow_blocked,
type: :boolean,
description: "Whether blocks result in people getting unfollowed"
},
%{
key: :outgoing_blocks,
type: :boolean,
description: "Whether to federate blocks to other instances"
},
%{
key: :sign_object_fetches,
type: :boolean,
description: "Sign object fetches with HTTP signatures"
},
%{
key: :note_replies_output_limit,
type: :integer,
description:
"The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)."
},
%{
key: :follow_handshake_timeout,
type: :integer,
description: "Following handshake timeout",
suggestions: [500]
}
]
},
%{
group: :pleroma,
key: :http_security,
type: :group,
description: "HTTP security settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the managed content security policy is enabled"
},
%{
key: :sts,
label: "STS",
type: :boolean,
description: "Whether to additionally send a Strict-Transport-Security header"
},
%{
key: :sts_max_age,
label: "STS max age",
type: :integer,
description: "The maximum age for the Strict-Transport-Security header if sent",
suggestions: [31_536_000]
},
%{
key: :ct_max_age,
label: "CT max age",
type: :integer,
description: "The maximum age for the Expect-CT header if sent",
suggestions: [2_592_000]
},
%{
key: :referrer_policy,
type: :string,
description: "The referrer policy to use, either \"same-origin\" or \"no-referrer\"",
suggestions: ["same-origin", "no-referrer"]
},
%{
key: :report_uri,
label: "Report URI",
type: :string,
description: "Adds the specified url to report-uri and report-to group in CSP header",
suggestions: ["https://example.com/report-uri"]
}
]
},
%{
group: :web_push_encryption,
key: :vapid_details,
type: :group,
description:
"Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it",
children: [
%{
key: :subject,
type: :string,
description:
"A mailto link for the administrative contact." <>
" It's best if this email is not a personal email address, but rather a group email to the instance moderation team.",
suggestions: ["mailto:moderators@pleroma.com"]
},
%{
key: :public_key,
type: :string,
description: "VAPID public key",
suggestions: ["Public key"]
},
%{
key: :private_key,
type: :string,
description: "VAPID private key",
suggestions: ["Private key"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha,
type: :group,
description: "Captcha-related settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the captcha should be shown on registration"
},
%{
key: :method,
type: :module,
description: "The method/service to use for captcha",
suggestions: [Pleroma.Captcha.Kocaptcha, Pleroma.Captcha.Native]
},
%{
key: :seconds_valid,
type: :integer,
description: "The time in seconds for which the captcha is valid",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha.Kocaptcha,
type: :group,
description:
"Kocaptcha is a very simple captcha service with a single API endpoint, the source code is" <>
" here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer.",
children: [
%{
key: :endpoint,
type: :string,
description: "The kocaptcha endpoint to use",
suggestions: ["https://captcha.kotobank.ch"]
}
]
},
%{
group: :pleroma,
type: :group,
description:
"Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter",
children: [
%{
key: :admin_token,
type: :string,
description: "Token",
suggestions: ["We recommend a secure random string or UUID"]
}
]
},
%{
group: :pleroma,
key: Oban,
type: :group,
description: """
[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.
Note: 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).
""",
children: [
%{
key: :repo,
type: :module,
description: "Application's Ecto repo",
suggestions: [Pleroma.Repo]
},
%{
key: :verbose,
type: {:dropdown, :atom},
description: "Logs verbose mode",
suggestions: [false, :error, :warn, :info, :debug]
},
%{
key: :prune,
type: [:atom, :tuple],
description:
"Non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning)",
suggestions: [:disabled, {:maxlen, 1500}, {:maxage, 60 * 60}]
},
%{
key: :queues,
type: {:keyword, :integer},
description:
"Background jobs queues (keys: queues, values: max numbers of concurrent jobs)",
suggestions: [
activity_expiration: 10,
attachments_cleanup: 5,
background: 5,
federator_incoming: 50,
federator_outgoing: 50,
mailer: 10,
scheduled_activities: 10,
transmogrifier: 20,
web_push: 50
],
children: [
%{
key: :activity_expiration,
type: :integer,
description: "Activity expiration queue",
suggestions: [10]
},
%{
key: :attachments_cleanup,
type: :integer,
description: "Attachment deletion queue",
suggestions: [5]
},
%{
key: :background,
type: :integer,
description: "Background queue",
suggestions: [5]
},
%{
key: :federator_incoming,
type: :integer,
description: "Incoming federation queue",
suggestions: [50]
},
%{
key: :federator_outgoing,
type: :integer,
description: "Outgoing federation queue",
suggestions: [50]
},
%{
key: :mailer,
type: :integer,
description: "Email sender queue, see Pleroma.Emails.Mailer",
suggestions: [10]
},
%{
key: :scheduled_activities,
type: :integer,
description: "Scheduled activities queue, see Pleroma.ScheduledActivities",
suggestions: [10]
},
%{
key: :transmogrifier,
type: :integer,
description: "Transmogrifier queue",
suggestions: [20]
},
%{
key: :web_push,
type: :integer,
description: "Web push notifications queue",
suggestions: [50]
}
]
},
%{
key: :crontab,
type: {:list, :tuple},
description: "Settings for cron background jobs",
suggestions: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
{"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
}
]
},
%{
group: :pleroma,
key: :workers,
type: :group,
description: "Includes custom worker options not interpretable directly by `Oban`",
children: [
%{
key: :retries,
type: {:keyword, :integer},
description: "Max retry attempts for failed jobs, per `Oban` queue",
suggestions: [
federator_incoming: 5,
federator_outgoing: 5
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Metadata,
type: :group,
description: "Metadata-related settings",
children: [
%{
key: :providers,
type: {:list, :module},
description: "List of metadata providers to enable",
suggestions: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.Feed
]
},
%{
key: :unfurl_nsfw,
label: "Unfurl NSFW",
type: :boolean,
description: "When enabled NSFW attachments will be shown in previews"
}
]
},
%{
group: :pleroma,
key: :rich_media,
type: :group,
description:
"If enabled the instance will parse metadata from attached links to generate link previews.",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables RichMedia parsing of URLs."
},
%{
key: :ignore_hosts,
type: {:list, :string},
description: "List of hosts which will be ignored by the metadata parser.",
suggestions: ["accounts.google.com", "xss.website"]
},
%{
key: :ignore_tld,
label: "Ignore TLD",
type: {:list, :string},
description: "List TLDs (top-level domains) which will ignore for parse metadata.",
suggestions: ["local", "localdomain", "lan"]
},
%{
key: :parsers,
type: {:list, :module},
description: "List of Rich Media parsers.",
suggestions: [
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
Pleroma.Web.RichMedia.Parsers.OEmbed,
Pleroma.Web.RichMedia.Parsers.OGP,
Pleroma.Web.RichMedia.Parsers.TwitterCard
]
},
%{
key: :ttl_setters,
label: "TTL setters",
type: {:list, :module},
description: "List of rich media TTL setters.",
suggestions: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
]
}
]
},
%{
group: :auto_linker,
key: :opts,
type: :group,
description: "Configuration for the auto_linker library",
children: [
%{
key: :class,
type: [:string, false],
description: "Specify the class to be added to the generated link. Disable to clear",
suggestions: ["auto-linker", false]
},
%{
key: :rel,
type: [:string, false],
description: "Override the rel attribute. Disable to clear",
suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
type: :boolean,
description: "Link urls will open in new window/tab"
},
%{
key: :truncate,
type: [:integer, false],
description:
"Set to a number to truncate urls longer then the number. Truncated urls will end in `..`",
suggestions: [15, false]
},
%{
key: :strip_prefix,
type: :boolean,
description: "Strip the scheme prefix"
},
%{
key: :extra,
type: :boolean,
description: "Link urls with rarely used schemes (magnet, ipfs, irc, etc.)"
}
]
},
%{
group: :pleroma,
key: Pleroma.ScheduledActivity,
type: :group,
description: "Scheduled activities settings",
children: [
%{
key: :daily_user_limit,
type: :integer,
description:
"The number of scheduled activities a user is allowed to create in a single day. Default: 25.",
suggestions: [25]
},
%{
key: :total_user_limit,
type: :integer,
description:
"The number of scheduled activities a user is allowed to create in total. Default: 300.",
suggestions: [300]
},
%{
key: :enabled,
type: :boolean,
description: "Whether scheduled activities are sent to the job queue to be executed"
}
]
},
%{
group: :pleroma,
key: Pleroma.ActivityExpiration,
type: :group,
description: "Expired activity settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether expired activities will be sent to the job queue to be deleted"
}
]
},
%{
group: :pleroma,
type: :group,
description: "Authenticator",
children: [
%{
key: Pleroma.Web.Auth.Authenticator,
type: :module,
suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
}
]
},
%{
group: :pleroma,
key: :ldap,
type: :group,
description:
"Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password" <>
" will be verified by trying to authenticate (bind) to a LDAP server." <>
" If a user exists in the LDAP directory but there is no account with the same name yet on the" <>
" Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name.",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables LDAP authentication"
},
%{
key: :host,
type: :string,
description: "LDAP server hostname",
suggestions: ["localhosts"]
},
%{
key: :port,
type: :integer,
description: "LDAP port, e.g. 389 or 636",
suggestions: [389, 636]
},
%{
key: :ssl,
label: "SSL",
type: :boolean,
description: "Enable to use SSL, usually implies the port 636"
},
%{
key: :sslopts,
label: "SSL options",
type: :keyword,
description: "Additional SSL options",
suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer],
children: [
%{
key: :cacertfile,
type: :string,
description: "Path to file with PEM encoded cacerts",
suggestions: ["path/to/file/with/PEM/cacerts"]
},
%{
key: :verify,
type: :atom,
description: "Type of cert verification",
suggestions: [:verify_peer]
}
]
},
%{
key: :tls,
label: "TLS",
type: :boolean,
description: "Enable to use STARTTLS, usually implies the port 389"
},
%{
key: :tlsopts,
label: "TLS options",
type: :keyword,
description: "Additional TLS options",
suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer],
children: [
%{
key: :cacertfile,
type: :string,
description: "Path to file with PEM encoded cacerts",
suggestions: ["path/to/file/with/PEM/cacerts"]
},
%{
key: :verify,
type: :atom,
description: "Type of cert verification",
suggestions: [:verify_peer]
}
]
},
%{
key: :base,
type: :string,
description: "LDAP base, e.g. \"dc=example,dc=com\"",
suggestions: ["dc=example,dc=com"]
},
%{
key: :uid,
type: :string,
description:
"LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"",
suggestions: ["cn"]
}
]
},
%{
group: :pleroma,
key: :auth,
type: :group,
description: "Authentication / authorization settings",
children: [
%{
key: :enforce_oauth_admin_scope_usage,
type: :boolean,
description:
"OAuth admin scope requirement toggle. " <>
"If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
"(client app must support admin scopes). If disabled and token doesn't have admin scope(s)," <>
"`is_admin` user flag grants access to admin-specific actions."
},
%{
key: :auth_template,
type: :string,
description:
"Authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`.",
suggestions: ["show.html"]
},
%{
key: :oauth_consumer_template,
type: :string,
description:
"OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to" <>
" `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.",
suggestions: ["consumer.html"]
},
%{
key: :oauth_consumer_strategies,
type: {:list, :string},
description:
"The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
" Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\"" <>
" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>).",
suggestions: ["twitter", "keycloak:ueberauth_keycloak_strategy"]
}
]
},
%{
group: :pleroma,
key: :email_notifications,
type: :group,
description: "Email notifications settings",
children: [
%{
key: :digest,
type: :map,
description:
"emails of \"what you've missed\" for users who have been inactive for a while",
suggestions: [
%{
active: false,
schedule: "0 0 * * 0",
interval: 7,
inactivity_threshold: 7
}
],
children: [
%{
key: :active,
+ label: "Enabled",
type: :boolean,
description: "Globally enable or disable digest emails"
},
%{
key: :schedule,
type: :string,
description:
"When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\".",
suggestions: ["0 0 * * 0"]
},
%{
key: :interval,
type: :integer,
description: "Minimum interval between digest emails to one user",
suggestions: [7]
},
%{
key: :inactivity_threshold,
type: :integer,
description: "Minimum user inactivity threshold",
suggestions: [7]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.UserEmail,
type: :group,
description: "Email template settings",
children: [
%{
key: :logo,
type: :string,
description: "A path to a custom logo. Set it to `nil` to use the default Pleroma logo.",
suggestions: ["some/path/logo.png"]
},
%{
key: :styling,
type: :map,
description: "a map with color settings for email templates.",
suggestions: [
%{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
],
children: [
%{
key: :link_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :background_color,
type: :string,
suggestions: ["#2C3645"]
},
%{
key: :content_background_color,
type: :string,
suggestions: ["#1B2635"]
},
%{
key: :header_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :text_color,
type: :string,
suggestions: ["#b9b9ba"]
},
%{
key: :text_muted_color,
type: :string,
suggestions: ["#b9b9ba"]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.NewUsersDigestEmail,
type: :group,
description: "New users admin email digest",
children: [
%{
key: :enabled,
type: :boolean,
description: "enables new users admin digest email when `true`",
suggestions: [false]
}
]
},
%{
group: :pleroma,
key: :oauth2,
type: :group,
description: "Configure OAuth 2 provider capabilities",
children: [
%{
key: :token_expires_in,
type: :integer,
description: "The lifetime in seconds of the access token",
suggestions: [600]
},
%{
key: :issue_new_refresh_token,
type: :boolean,
description:
"Keeps old refresh token or generate new refresh token when to obtain an access token"
},
%{
key: :clean_expired_tokens,
type: :boolean,
description: "Enable a background job to clean expired oauth tokens. Default: disabled."
}
]
},
%{
group: :pleroma,
key: :emoji,
type: :group,
children: [
%{
key: :shortcode_globs,
type: {:list, :string},
description: "Location of custom emoji files. * can be used as a wildcard.",
suggestions: ["/emoji/custom/**/*.png"]
},
%{
key: :pack_extensions,
type: {:list, :string},
description:
"A list of file extensions for emojis, when no emoji.txt for a pack is present",
suggestions: [".png", ".gif"]
},
%{
key: :groups,
type: {:keyword, :string, {:list, :string}},
description:
"Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name" <>
" and the value is the location or array of locations. * can be used as a wildcard.",
suggestions: [
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
]
},
%{
key: :default_manifest,
type: :string,
description:
"Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download." <>
" Currently only one manifest can be added (no arrays).",
suggestions: ["https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"]
},
%{
key: :shared_pack_cache_seconds_per_file,
label: "Shared pack cache s/file",
type: :integer,
descpiption:
"When an emoji pack is shared, the archive is created and cached in memory" <>
" for this amount of seconds multiplied by the number of files.",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: :rate_limit,
type: :group,
description:
"Rate limit settings. This is an advanced feature enabled only for :authentication by default.",
children: [
%{
key: :search,
type: [:tuple, {:list, :tuple}],
description: "For the search requests (account & status search etc.)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :timeline,
type: [:tuple, {:list, :tuple}],
description: "For requests to timelines (each timeline has it's own limiter)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :app_account_creation,
type: [:tuple, {:list, :tuple}],
description: "For registering user accounts from the same IP address",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relations_actions,
type: [:tuple, {:list, :tuple}],
description: "For actions on relationships with all users (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relation_id_action,
type: [:tuple, {:list, :tuple}],
description: "For actions on relation with a specific user (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :statuses_actions,
type: [:tuple, {:list, :tuple}],
description:
"For create / delete / fav / unfav / reblog / unreblog actions on any statuses",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :status_id_action,
type: [:tuple, {:list, :tuple}],
description:
"For fav / unfav or reblog / unreblog actions on the same status by the same user",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :authentication,
type: [:tuple, {:list, :tuple}],
description: "For authentication create / password check / user existence check requests",
suggestions: [{60_000, 15}]
}
]
},
%{
group: :esshd,
type: :group,
description:
"Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <>
"and generate host keys in your priv dir with ssh-keygen -m PEM -N \"\" -b 2048 -t rsa -f ssh_host_rsa_key",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables SSH"
},
%{
key: :priv_dir,
type: :string,
description: "Dir with SSH keys",
suggestions: ["/some/path/ssh_keys"]
},
%{
key: :handler,
type: :string,
description: "Handler module",
suggestions: ["Pleroma.BBS.Handler"]
},
%{
key: :port,
type: :integer,
description: "Port to connect",
suggestions: [10_022]
},
%{
key: :password_authenticator,
type: :string,
description: "Authenticator module",
suggestions: ["Pleroma.BBS.Authenticator"]
}
]
},
%{
group: :mime,
type: :group,
description: "Mime types",
children: [
%{
key: :types,
type: :map,
suggestions: [
%{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
],
children: [
%{
key: "application/xml",
type: {:list, :string},
suggestions: ["xml"]
},
%{
key: "application/xrd+xml",
type: {:list, :string},
suggestions: ["xrd+xml"]
},
%{
key: "application/jrd+json",
type: {:list, :string},
suggestions: ["jrd+json"]
},
%{
key: "application/activity+json",
type: {:list, :string},
suggestions: ["activity+json"]
},
%{
key: "application/ld+json",
type: {:list, :string},
suggestions: ["activity+json"]
}
]
}
]
},
%{
group: :pleroma,
key: :chat,
type: :group,
description: "Pleroma chat settings",
children: [
%{
key: :enabled,
type: :boolean
}
]
},
%{
group: :http_signatures,
type: :group,
description: "HTTP Signatures settings",
children: [
%{
key: :adapter,
type: :module,
suggestions: [Pleroma.Signature]
}
]
},
%{
group: :pleroma,
key: :http,
type: :group,
description: "HTTP settings",
children: [
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["localhost:9020", {:socks5, :localhost, 3090}]
},
%{
key: :send_user_agent,
type: :boolean
},
%{
key: :user_agent,
type: [:string, :atom],
description:
"What user agent to use. Must be a string or an atom `:default`. Default value is `:default`.",
suggestions: ["Pleroma", :default]
},
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
suggestions: [],
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS version to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
}
]
}
]
}
]
},
%{
group: :pleroma,
key: :markup,
type: :group,
children: [
%{
key: :allow_inline_images,
type: :boolean
},
%{
key: :allow_headings,
type: :boolean
},
%{
key: :allow_tables,
type: :boolean
},
%{
key: :allow_fonts,
type: :boolean
},
%{
key: :scrub_policy,
type: {:list, :module},
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
}
]
},
%{
group: :pleroma,
key: :user,
type: :group,
children: [
%{
key: :deny_follow_blocked,
type: :boolean
}
]
},
%{
group: :pleroma,
key: :mrf_normalize_markup,
label: "MRF normalize markup",
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
type: :group,
children: [
%{
key: :scrub_policy,
type: :module,
suggestions: [Pleroma.HTML.Scrubber.Default]
}
]
},
%{
group: :pleroma,
key: Pleroma.User,
type: :group,
children: [
%{
key: :restricted_nicknames,
type: {:list, :string},
suggestions: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web"
]
}
]
},
%{
group: :cors_plug,
type: :group,
children: [
%{
key: :max_age,
type: :integer,
suggestions: [86_400]
},
%{
key: :methods,
type: {:list, :string},
suggestions: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]
},
%{
key: :expose,
type: {:list, :string},
suggestions: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
]
},
%{
key: :credentials,
type: :boolean
},
%{
key: :headers,
type: {:list, :string},
suggestions: ["Authorization", "Content-Type", "Idempotency-Key"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Plugs.RemoteIp,
type: :group,
description: """
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
""",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enable/disable the plug. Default: disabled."
},
%{
key: :headers,
type: {:list, :string},
description:
"A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Default: `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`."
},
%{
key: :proxies,
type: {:list, :string},
description:
"A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Default: `[]`."
},
%{
key: :reserved,
type: {:list, :string},
description:
"Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)."
}
]
},
%{
group: :pleroma,
key: :web_cache_ttl,
type: :group,
description:
"The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.",
children: [
%{
key: :activity_pub,
type: :integer,
description:
"Activity pub routes (except question activities). Default: `nil` (no expiration).",
suggestions: [30_000, nil]
},
%{
key: :activity_pub_question,
type: :integer,
description: "Activity pub routes (question activities). Default: `30_000` (30 seconds).",
suggestions: [30_000]
}
]
},
%{
group: :pleroma,
key: :static_fe,
type: :group,
description:
"Render profiles and posts using server-generated HTML that is viewable without using JavaScript.",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the rendering of static HTML. Default: disabled."
}
]
},
%{
group: :pleroma,
key: :feed,
type: :group,
description: "Configure feed rendering",
children: [
%{
key: :post_title,
type: :map,
description: "Configure title rendering.",
children: [
%{
key: :max_length,
type: :integer,
description: "Maximum number of characters before truncating title.",
suggestions: [100]
},
%{
key: :omission,
type: :string,
description: "Replacement which will be used after truncating string.",
suggestions: ["..."]
}
]
}
]
},
%{
group: :pleroma,
key: :mrf_object_age,
type: :group,
description: "Rejects or delists posts based on their age when received.",
children: [
%{
key: :threshold,
type: :integer,
description: "Required age (in seconds) of a post before actions are taken.",
suggestions: [172_800]
},
%{
key: :actions,
type: {:list, :atom},
description:
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
"`:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines; " <>
"`:reject` rejects the message entirely",
suggestions: [:delist, :strip_followers, :reject]
}
]
},
%{
group: :pleroma,
key: :modules,
type: :group,
description: "Custom Runtime Modules",
children: [
%{
key: :runtime_dir,
type: :string,
description: "A path to custom Elixir modules (such as MRF policies)."
}
]
},
%{
group: :pleroma,
key: :streamer,
type: :group,
description: "Settings for notifications streamer",
children: [
%{
key: :workers,
type: :integer,
description: "Number of workers to send notifications.",
suggestions: [3]
},
%{
key: :overflow_workers,
type: :integer,
description: "Maximum number of workers created if pool is empty.",
suggestions: [2]
}
]
},
%{
group: :pleroma,
key: :connections_pool,
type: :group,
description: "Advanced settings for `gun` connections pool",
children: [
%{
key: :checkin_timeout,
type: :integer,
description: "Timeout to checkin connection from pool. Default: 250ms.",
suggestions: [250]
},
%{
key: :max_connections,
type: :integer,
description: "Maximum number of connections in the pool. Default: 250 connections.",
suggestions: [250]
},
%{
key: :retry,
type: :integer,
description:
"Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.",
suggestions: [1]
},
%{
key: :retry_timeout,
type: :integer,
description:
"Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.",
suggestions: [1000]
},
%{
key: :await_up_timeout,
type: :integer,
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
suggestions: [5000]
}
]
},
%{
group: :pleroma,
key: :pools,
type: :group,
description: "Advanced settings for `gun` workers pools",
children: [
%{
key: :federation,
type: :keyword,
description: "Settings for federation pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [10]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :media,
type: :keyword,
description: "Settings for media pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [10]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :upload,
type: :keyword,
description: "Settings for upload pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [25]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [5]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [300_000]
}
]
},
%{
key: :default,
type: :keyword,
description: "Settings for default pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [10]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [2]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [10_000]
}
]
}
]
},
%{
group: :pleroma,
key: :hackney_pools,
type: :group,
description: "Advanced settings for `hackney` connections pools",
children: [
%{
key: :federation,
type: :keyword,
description: "Settings for federation pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :media,
type: :keyword,
description: "Settings for media pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :upload,
type: :keyword,
description: "Settings for upload pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [25]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [300_000]
}
]
}
]
},
%{
group: :pleroma,
key: :restrict_unauthenticated,
type: :group,
description:
"Disallow viewing timelines, user profiles and statuses for unauthenticated users.",
children: [
%{
key: :timelines,
type: :map,
description: "Settings for public and federated timelines.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view public timeline."
},
%{
key: :federated,
type: :boolean,
description: "Disallow view federated timeline."
}
]
},
%{
key: :profiles,
type: :map,
description: "Settings for user profiles.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local user profiles."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote user profiles."
}
]
},
%{
key: :activities,
type: :map,
description: "Settings for statuses.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local statuses."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote statuses."
}
]
}
]
+ },
+ %{
+ group: :pleroma,
+ key: Pleroma.Web.ApiSpec.CastAndValidate,
+ type: :group,
+ children: [
+ %{
+ key: :strict,
+ type: :boolean,
+ description:
+ "Enables strict input validation (useful in development, not recommended in production)",
+ suggestions: [false]
+ }
+ ]
}
]
diff --git a/config/dev.exs b/config/dev.exs
index 7e1e3b4be..4faaeff5b 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -1,64 +1,66 @@
use Mix.Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint,
http: [
port: 4000,
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
],
protocol: "http",
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [],
secure_cookie_flag: false
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# command from your terminal:
#
# openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
#
# The `http:` config above can be replaced with:
#
# https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "pleroma_dev",
hostname: "localhost",
pool_size: 10
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs"
else
IO.puts(
"!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs"
)
end
if File.exists?("./config/dev.exported_from_db.secret.exs"),
do: import_config("dev.exported_from_db.secret.exs")
diff --git a/config/test.exs b/config/test.exs
index 040e67e4a..cbf775109 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,105 +1,107 @@
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4001],
url: [port: 4001],
server: true
# Disable captha for tests
config :pleroma, Pleroma.Captcha,
# It should not be enabled for automatic tests
enabled: false,
# A fake captcha service for tests
method: Pleroma.Captcha.Mock
# Print only warnings and errors during test
config :logger, :console,
level: :warn,
format: "\n[$level] $message\n"
config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload, filters: [], link_name: false
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
config :pleroma, :instance,
email: "admin@example.com",
notify_email: "noreply@example.com",
skip_thread_containment: false,
federating: false,
external_user_synchronization: false,
static_dir: "test/instance_static/"
config :pleroma, :activitypub, sign_object_fetches: false
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "pleroma_test",
hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1
config :tesla, adapter: Tesla.Mock
config :pleroma, :rich_media,
enabled: false,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"]
config :web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma, Oban,
queues: false,
prune: :disabled,
crontab: false
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2,
total_user_limit: 3,
enabled: false
config :pleroma, :rate_limit, %{}
config :pleroma, :http_security, report_uri: "https://endpoint.com"
config :pleroma, :http, send_user_agent: false
rum_enabled = System.get_env("RUM_ENABLED") == "true"
config :pleroma, :database, rum_enabled: rum_enabled
IO.puts("RUM enabled: #{rum_enabled}")
config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp35v0RK9SO8WTPr6QZ"
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
config :pleroma, :modules, runtime_dir: "test/fixtures/modules"
config :pleroma, Pleroma.Gun, Pleroma.GunMock
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else
IO.puts(
"You may want to create test.secret.exs to declare custom database connection parameters."
)
end
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index 6202c5a1a..23af08961 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -1,1191 +1,1202 @@
# Admin API
Authentication is required and the user must be an admin.
Configuration options:
* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
Note that client app needs to explicitly support admin scopes and request them when obtaining auth token.
## `GET /api/pleroma/admin/users`
### List users
- Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters:
- `local`: only local users
- `external`: only external users
- `active`: only active users
- `deactivated`: only deactivated users
- `is_admin`: users with admin role
- `is_moderator`: users with moderator role
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`)
- *optional* `tags`: **[string]** tags list
- *optional* `name`: **string** user display name
- *optional* `email`: **string** user email
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- Response:
```json
{
"page_size": integer,
"count": integer,
"users": [
{
"deactivated": bool,
"id": integer,
"nickname": string,
"roles": {
"admin": bool,
"moderator": bool
},
"local": bool,
"tags": array,
"avatar": string,
"display_name": string
},
...
]
}
```
## DEPRECATED `DELETE /api/pleroma/admin/users`
### Remove a user
- Params:
- `nickname`
- Response: User’s nickname
## `DELETE /api/pleroma/admin/users`
### Remove a user
- Params:
- `nicknames`
- Response: Array of user nicknames
### Create a user
- Method: `POST`
- Params:
`users`: [
{
`nickname`,
`email`,
`password`
}
]
- Response: User’s nickname
## `POST /api/pleroma/admin/users/follow`
### Make a user follow another user
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `POST /api/pleroma/admin/users/unfollow`
### Make a user unfollow another user
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
- Params:
- `nickname`
- Response: User’s object
```json
{
"deactivated": bool,
"id": integer,
"nickname": string
}
```
## `PUT /api/pleroma/admin/users/tag`
### Tag a list of users
- Params:
- `nicknames` (array)
- `tags` (array)
## `DELETE /api/pleroma/admin/users/tag`
### Untag a list of users
- Params:
- `nicknames` (array)
- `tags` (array)
## `GET /api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
## `GET /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist.
### Get user user permission groups membership per permission group
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Add user to permission group
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
## `POST /api/pleroma/admin/users/permission_group/:permission_group`
### Add users to permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
- Note: An admin cannot revoke their own admin status.
## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
### Remove users from permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
- Note: An admin cannot revoke their own admin status.
## `PATCH /api/pleroma/admin/users/activate`
### Activate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `PATCH /api/pleroma/admin/users/deactivate`
### Deactivate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user
- Params:
- `nickname` or `id`
- Response:
- On failure: `Not found`
- On success: JSON of the user
## `GET /api/pleroma/admin/users/:nickname_or_id/statuses`
### Retrive user's latest statuses
- Params:
- `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` – allows to see private statuses
- *optional* `with_reblogs`: `true`/`false` – allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- On success: JSON array of user's latest statuses
## `GET /api/pleroma/admin/instances/:instance/statuses`
### Retrive instance's latest statuses
- Params:
- `instance`: instance name
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` – allows to see private statuses
- *optional* `with_reblogs`: `true`/`false` – allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- On success: JSON array of instance's latest statuses
## `GET /api/pleroma/admin/statuses`
### Retrives all latest statuses
- Params:
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `local_only`: excludes remote statuses
- *optional* `godmode`: `true`/`false` – allows to see private statuses
- *optional* `with_reblogs`: `true`/`false` – allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- On success: JSON array of user's latest statuses
## `POST /api/pleroma/admin/relay`
### Follow a Relay
- Params:
- `relay_url`
- Response:
- On success: URL of the followed relay
## `DELETE /api/pleroma/admin/relay`
### Unfollow a Relay
- Params:
- `relay_url`
- Response:
- On success: URL of the unfollowed relay
## `GET /api/pleroma/admin/relay`
### List Relays
- Params: none
- Response:
- On success: JSON array of relays
## `POST /api/pleroma/admin/users/invite_token`
### Create an account registration invite token
- Params:
- *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07")
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `GET /api/pleroma/admin/users/invites`
### Get a list of generated invites
- Params: none
- Response:
```json
{
"invites": [
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
},
...
]
}
```
## `POST /api/pleroma/admin/users/revoke_invite`
### Revoke invite by token
- Params:
- `token`
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `POST /api/pleroma/admin/users/email_invite`
### Sends registration invite via email
- Params:
- `email`
- `name`, optional
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request, JSON:
```json
[
{
"error": "Appropriate error message here"
}
]
```
## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname
- Params: none
- Response:
```json
{
"token": "base64 reset token",
"link": "https://pleroma.social/api/pleroma/password_reset/url-encoded-base64-token"
}
```
## `PATCH /api/pleroma/admin/users/force_password_reset`
### Force passord reset for a user with a given nickname
- Params:
- `nicknames`
- Response: none (code `204`)
## `GET /api/pleroma/admin/users/:nickname/credentials`
### Get the user's email, password, display and settings-related fields
- Params:
- `nickname`
- Response:
```json
{
"actor_type": "Person",
"allow_following_move": true,
"avatar": "https://pleroma.social/media/7e8e7508fd545ef580549b6881d80ec0ff2c81ed9ad37b9bdbbdf0e0d030159d.jpg",
"background": "https://pleroma.social/media/4de34c0bd10970d02cbdef8972bef0ebbf55f43cadc449554d4396156162fe9a.jpg",
"banner": "https://pleroma.social/media/8d92ba2bd244b613520abf557dd448adcd30f5587022813ee9dd068945986946.jpg",
"bio": "bio",
"default_scope": "public",
"discoverable": false,
"email": "user@example.com",
"fields": [
{
"name": "example",
"value": "<a href=\"https://example.com\" rel=\"ugc\">https://example.com</a>"
}
],
"hide_favorites": false,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"id": "9oouHaEEUR54hls968",
"locked": true,
"name": "user",
"no_rich_text": true,
"pleroma_settings_store": {},
"raw_fields": [
{
"id": 1,
"name": "example",
"value": "https://example.com"
},
],
"show_role": true,
"skip_thread_containment": false
}
```
## `PATCH /api/pleroma/admin/users/:nickname/credentials`
### Change the user's email, password, display and settings-related fields
- Params:
- `email`
- `password`
- `name`
- `bio`
- `avatar`
- `locked`
- `no_rich_text`
- `default_scope`
- `banner`
- `hide_follows`
- `hide_followers`
- `hide_followers_count`
- `hide_follows_count`
- `hide_favorites`
- `allow_following_move`
- `background`
- `show_role`
- `skip_thread_containment`
- `fields`
- `discoverable`
- `actor_type`
- Response: none (code `200`)
## `GET /api/pleroma/admin/reports`
### Get a list of reports
- Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
- `account`: the user who has been reported
- `actor`: the user who has sent the report
- `statuses`: list of statuses that have been included to the report
```json
{
"totalReports" : 1,
"reports": [
{
"account": {
"acct": "user",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-04-23T17:32:04.000Z",
"display_name": "User",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9i6dAJqSGSKMzLG2Lo",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": true,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 3,
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
"actor": {
"acct": "lain",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-03-28T17:36:03.000Z",
"display_name": "Roger Braun",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9hEkA5JsvAdlSrocam",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": false,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 1,
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
"state": "open",
"statuses": [
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `GET /api/pleroma/admin/grouped_reports`
### Get a list of reports, grouped by status
- Params: none
- On success: JSON, returns a list of reports, where:
- `date`: date of the latest report
- `account`: the user who has been reported (see `/api/pleroma/admin/reports` for reference)
- `status`: reported status (see `/api/pleroma/admin/reports` for reference)
- `actors`: users who had reported this status (see `/api/pleroma/admin/reports` for reference)
- `reports`: reports (see `/api/pleroma/admin/reports` for reference)
```json
"reports": [
{
"date": "2019-10-07T12:31:39.615149Z",
"account": { ... },
"status": { ... },
"actors": [{ ... }, { ... }],
"reports": [{ ... }]
}
]
```
## `GET /api/pleroma/admin/reports/:id`
### Get an individual report
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `PATCH /api/pleroma/admin/reports`
### Change the state of one or multiple reports
- Params:
```json
`reports`: [
{
`id`, // required, report id
`state` // required, the new state. Valid values are `open`, `closed` and `resolved`
},
...
]
```
- Response:
- On failure:
- 400 Bad Request, JSON:
```json
[
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/notes`
### Create report note
- Params:
- `id`: required, report id
- `content`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
### Delete report note
- Params:
- `report_id`: required, report id
- `id`: required, note id
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
+## `GET /api/pleroma/admin/statuses/:id`
+
+### Show status by id
+
+- Params:
+ - `id`: required, status id
+- Response:
+ - On failure:
+ - 404 Not Found `"Not Found"`
+ - On success: JSON, Mastodon Status entity
+
## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `DELETE /api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`
## `GET /api/pleroma/admin/restart`
### Restarts pleroma application
**Only works when configuration from database is enabled.**
- Params: none
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{}
```
## `GET /api/pleroma/admin/need_reboot`
### Returns the flag whether the pleroma should be restarted
- Params: none
- Response:
- `need_reboot` - boolean
```json
{
"need_reboot": false
}
```
## `GET /api/pleroma/admin/config`
### Get list of merged default settings with saved in database.
*If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.**
- Params:
- `only_db`: true (*optional*, get only saved in database settings)
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{
"configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": []
}
],
"need_reboot": true
}
```
## `POST /api/pleroma/admin/config`
### Update config settings
*If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.**
Some modifications are necessary to save the config settings correctly:
- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules;
```
"Pleroma.Upload" -> Pleroma.Upload
"Oban" -> Oban
```
- strings starting with `:` will be converted to atoms;
```
":pleroma" -> :pleroma
```
- objects with `tuple` key and array value will be converted to tuples;
```
{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []}
```
- arrays with *tuple objects* will be converted to keywords;
```
[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"]
```
Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
- all settings inside these keys:
- `:hackney_pools`
- `:connections_pool`
- `:pools`
- `:chat`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
- Params:
- `configs` - array of config objects
- config object params:
- `group` - string (**required**)
- `key` - string (**required**)
- `value` - string, [], {} or {"tuple": []} (**required**)
- `delete` - true (*optional*, if setting must be deleted)
- `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored)
*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.*
```
[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value
{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion
[subkey2: val2] \\ value after deletion
```
*Most of the settings can be partially updated through merge old values with new values, except settings value of which is list or is not keyword.*
Example of setting without keyword in value:
```elixir
config :tesla, :adapter, Tesla.Adapter.Hackney
```
List of settings which support only full update by key:
```elixir
@full_key_update [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]
```
List of settings which support only full update by subkey:
```elixir
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
{:pleroma, :workers, :retries},
{:pleroma, :mrf_subchain, :match_actor},
{:pleroma, :mrf_keyword, :replace}
]
```
*Settings without explicit key must be sended in separate config object params.*
```elixir
config :quack,
level: :debug,
meta: [:all],
...
```
```json
{
"configs": [
{"group": ":quack", "key": ":level", "value": ":debug"},
{"group": ":quack", "key": ":meta", "value": [":all"]},
...
]
}
```
- Request:
```json
{
"configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [
{"tuple": [":uploader", "Pleroma.Uploaders.Local"]},
{"tuple": [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
{"tuple": [":link_name", true]},
{"tuple": [":proxy_remote", false]},
{"tuple": [":proxy_opts", [
{"tuple": [":redirect_on_failure", false]},
{"tuple": [":max_body_length", 1048576]},
{"tuple": [":http", [
{"tuple": [":follow_redirect", true]},
{"tuple": [":pool", ":upload"]},
]]}
]
]},
{"tuple": [":dispatch", {
"tuple": ["/api/v1/streaming", "Pleroma.Web.MastodonAPI.WebsocketHandler", []]
}]}
]
}
]
}
```
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{
"configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [...]
}
],
"need_reboot": true
}
```
## ` GET /api/pleroma/admin/config/descriptions`
### Get JSON with config descriptions.
Loads json generated from `config/descriptions.exs`.
- Params: none
- Response:
```json
[{
"group": ":pleroma", // string
"key": "ModuleName", // string
"type": "group", // string or list with possible values,
"description": "Upload general settings", // string
"children": [
{
"key": ":uploader", // string or module name `Pleroma.Upload`
"type": "module",
"description": "Module which will be used for uploads",
"suggestions": ["module1", "module2"]
},
{
"key": ":filters",
"type": ["list", "module"],
"description": "List of filter modules for uploads",
"suggestions": [
"module1", "module2", "module3"
]
}
]
}]
```
## `GET /api/pleroma/admin/moderation_log`
### Get moderation log
- Params:
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42`
- *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42
- *optional* `user_id`: **integer** filter logs by actor's id
- *optional* `search`: **string** search logs by the log message
- Response:
```json
[
{
"data": {
"actor": {
"id": 1,
"nickname": "lain"
},
"action": "relay_follow"
},
"time": 1502812026, // timestamp
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
}
]
```
## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji
- Authentication: required
- Params: None
- Response: JSON, "ok" and 200 status
## `PATCH /api/pleroma/admin/users/confirm_email`
### Confirm users' emails
- Params:
- `nicknames`
- Response: Array of user nicknames
## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
### Resend confirmation email
- Params:
- `nicknames`
- Response: Array of user nicknames
## `GET /api/pleroma/admin/stats`
### Stats
- Response:
```json
{
"status_visibility": {
"direct": 739,
"private": 9,
"public": 17,
"unlisted": 14
}
}
```
## `GET /api/pleroma/admin/oauth_app`
### List OAuth app
- Params:
- *optional* `name`
- *optional* `client_id`
- *optional* `page`
- *optional* `page_size`
- *optional* `trusted`
- Response:
```json
{
"apps": [
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
],
"count": 17,
"page_size": 50
}
```
## `POST /api/pleroma/admin/oauth_app`
### Create OAuth App
- Params:
- `name`
- `redirect_uris`
- `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
- On failure:
```json
{
"redirect_uris": "can't be blank",
"name": "can't be blank"
}
```
## `PATCH /api/pleroma/admin/oauth_app/:id`
### Update OAuth App
- Params:
- *optional* `name`
- *optional* `redirect_uris`
- *optional* `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
## `DELETE /api/pleroma/admin/oauth_app/:id`
### Delete OAuth App
- Params: None
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
\ No newline at end of file
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 681ab6b93..705c4c15e 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -1,927 +1,931 @@
# Configuration Cheat Sheet
This is a cheat sheet for Pleroma configuration file, any setting possible to configure should be listed here.
For OTP installations the configuration is typically stored in `/etc/pleroma/config.exs`.
For from source installations Pleroma configuration works by first importing the base config `config/config.exs`, then overriding it by the environment config `config/$MIX_ENV.exs` and then overriding it by user config `config/$MIX_ENV.secret.exs`. In from source installations you should always make the changes to the user config and NEVER to the base config to avoid breakages and merge conflicts. So for production you change/add configuration to `config/prod.secret.exs`.
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :instance
* `name`: The instance’s name.
* `email`: Email used to reach an Administrator/Moderator of the instance.
* `notify_email`: Email used for notifications.
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
* `limit`: Posts character limit (CW/Subject included in the counter).
* `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner).
* `avatar_upload_limit`: File size limit of user’s profile avatars.
* `background_upload_limit`: File size limit of user’s profile backgrounds.
* `banner_upload_limit`: File size limit of user’s profile banners.
* `poll_limits`: A map with poll limits for **local** polls.
* `max_options`: Maximum number of options.
* `max_option_chars`: Maximum number of characters per option.
* `min_expiration`: Minimum expiration time (in seconds).
* `max_expiration`: Maximum expiration time (in seconds).
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in.
* `federating`: Enable federation with other instances.
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
older software for theses nicknames.
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses.
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
* `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
* `remote_post_retention_days`: The default amount of days to retain remote posts when pruning the database.
* `user_bio_length`: A user bio maximum length (default: `5000`).
* `user_name_length`: A user name maximum length (default: `100`).
* `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`.
* `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`).
* `account_field_name_length`: An account field name maximum length (default: `512`).
* `account_field_value_length`: An account field value maximum length (default: `2048`).
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
## Federation
### MRF policies
!!! note
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `rewrite_policy` under [:instance](#instance) section.
#### :mrf_simple
* `media_removal`: List of instances to remove media from.
* `media_nsfw`: List of instances to put media as NSFW(sensitive) from.
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline.
* `reject`: List of instances to reject any activities from.
* `accept`: List of instances to accept any activities from.
* `report_removal`: List of instances to reject reports from.
* `avatar_removal`: List of instances to strip avatars from.
* `banner_removal`: List of instances to strip banners from.
#### :mrf_subchain
This policy processes messages through an alternate pipeline when a given message matches certain criteria.
All criteria are configured as a map of regular expressions to lists of policy modules.
* `match_actor`: Matches a series of regular expressions against the actor field.
Example:
```elixir
config :pleroma, :mrf_subchain,
match_actor: %{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
```
#### :mrf_rejectnonpublic
* `allow_followersonly`: whether to allow followers-only posts.
* `allow_direct`: whether to allow direct messages.
#### :mrf_hellthread
* `delist_threshold`: Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.
* `reject_threshold`: Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.
#### :mrf_keyword
* `reject`: A list of patterns which result in message being rejected, each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
* `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
* `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
#### :mrf_mention
* `actors`: A list of actors, for which to drop any posts mentioning.
#### :mrf_vocabulary
* `accept`: A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.
* `reject`: A list of ActivityStreams terms to reject. If empty, no messages are rejected.
#### :mrf_user_allowlist
The keys in this section are the domain names that the policy should apply to.
Each key should be assigned a list of users that should be allowed through by
their ActivityPub ID.
An example:
```elixir
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```
#### :mrf_object_age
* `threshold`: Required time offset (in seconds) compared to your server clock of an incoming post before actions are taken.
e.g., A value of 900 results in any post with a timestamp older than 15 minutes will be acted upon.
* `actions`: A list of actions to apply to the post:
* `:delist` removes the post from public timelines
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
* `sign_object_fetches`: Sign object fetches with HTTP signatures
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches
## Pleroma.ScheduledActivity
* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`)
* `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`)
* `enabled`: whether scheduled activities are sent to the job queue to be executed
## Pleroma.ActivityExpiration
* `enabled`: whether expired activities will be sent to the job queue to be deleted
## Frontends
### :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. 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`
To add your own configuration for PleromaFE, use it like this:
```elixir
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
# ... see /priv/static/static/config.json for the available keys.
},
masto_fe: %{
showInstanceSpecificPanel: true
}
```
These settings **need to be complete**, they will override the defaults.
### :static_fe
Render profiles and posts using server-generated HTML that is viewable without using JavaScript.
Available options:
* `enabled` - Enables the rendering of static HTML. Defaults to `false`.
### :assets
This section configures assets to be used with various frontends. Currently the only option
relates to mascots on the mastodon frontend
* `mascots`: KeywordList of mascots, each element __MUST__ contain both a `url` and a
`mime_type` key.
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
on MastoFE (default: `:pleroma_fox_tan`).
### :manifest
This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.
* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the
[spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it.
Example:
```elixir
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.png"
},
%{
src: "/static/icon.png",
type: "image/png"
},
%{
src: "/static/icon.ico",
sizes: "72x72 96x96 128x128 256x256"
}
]
```
* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`).
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`).
## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
* `shared_pack_cache_seconds_per_file`: When an emoji pack is shared, the archive is created and cached in
memory for this amount of seconds multiplied by the number of files.
## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy
## Link previews
### Pleroma.Web.Metadata (provider)
* `providers`: a list of metadata providers to enable. Providers available:
* `Pleroma.Web.Metadata.Providers.OpenGraph`
* `Pleroma.Web.Metadata.Providers.TwitterCard`
* `Pleroma.Web.Metadata.Providers.RelMe` - add links from user bio with rel=me into the `<header>` as `<link rel=me>`.
* `Pleroma.Web.Metadata.Providers.Feed` - add a link to a user's Atom feed into the `<header>` as `<link rel=alternate>`.
* `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews.
### :rich_media (consumer)
* `enabled`: if enabled the instance will parse metadata from attached links to generate link previews.
* `ignore_hosts`: list of hosts which will be ignored by the metadata parser. For example `["accounts.google.com", "xss.website"]`, defaults to `[]`.
* `ignore_tld`: list TLDs (top-level domains) which will ignore for parse metadata. default is ["local", "localdomain", "lan"].
* `parsers`: list of Rich Media parsers.
## HTTP server
### Pleroma.Web.Endpoint
!!! 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).
- `ip` - a tuple consisting of 4 integers
- `port`
* `url` - a list containing the configuration for generating urls, accepts
- `host` - the host without the scheme and a post (e.g `example.com`, not `https://example.com:2020`)
- `scheme` - e.g `http`, `https`
- `port`
- `path`
* `extra_cookie_attrs` - a list of `Key=Value` strings to be added as non-standard cookie attributes. Defaults to `["SameSite=Lax"]`. See the [SameSite article](https://www.owasp.org/index.php/SameSite) on OWASP for more info.
Example:
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "example.com", port: 2020, scheme: "https"],
http: [
port: 8080,
ip: {127, 0, 0, 1}
]
```
This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls starting with `https://example.com:2020`
### :http_security
* ``enabled``: Whether the managed content security policy is enabled.
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header.
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent.
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent.
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.
* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header.
### Pleroma.Plugs.RemoteIp
!!! 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.
Available options:
* `enabled` - Enable/disable the plug. Defaults to `false`.
* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `["x-forwarded-for"]`.
* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`.
* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network).
### :rate_limit
!!! note
If your instance is behind a reverse proxy ensure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default).
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
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.
For example:
```elixir
config :pleroma, :rate_limit,
authentication: {60_000, 15},
search: [{1000, 10}, {1000, 30}]
```
Means that:
1. In 60 seconds, 15 authentication attempts can be performed from the same IP address.
2. In 1 second, 10 search requests can be performed from the same IP adress by unauthenticated users, while authenticated users can perform 30 search requests per second.
Supported rate limiters:
* `:search` - Account/Status search.
* `:timeline` - Timeline requests (each timeline has it's own limiter).
* `:app_account_creation` - Account registration from the API.
* `:relations_actions` - Following/Unfollowing in general.
* `:relation_id_action` - Following/Unfollowing for a specific user.
* `:statuses_actions` - Status actions such as: (un)repeating, (un)favouriting, creating, deleting.
* `:status_id_action` - (un)Repeating/(un)Favouriting a particular status.
* `:authentication` - Authentication actions, i.e getting an OAuth token.
* `:password_reset` - Requesting password reset emails.
* `:account_confirmation_resend` - Requesting resending account confirmation emails.
* `:ap_routes` - Requesting statuses via ActivityPub.
### :web_cache_ttl
The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.
Available caches:
* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
## HTTP client
### :http
* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `adapter`: array of adapter options
### :hackney_pools
Advanced. Tweaks Hackney (http client) connections pools.
There's three pools used:
* `:federation` for the federation jobs.
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
* `:media` for rich media, media proxy
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
For each pool, the options are:
* `max_connections` - how much connections a pool can hold
* `timeout` - retention duration for connections
### :connections_pool
*For `gun` adapter*
Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools.
For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000.
It will increase memory usage, but federation would work faster.
* `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms.
* `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
* `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.
* `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.
* `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
### :pools
*For `gun` adapter*
Advanced settings for workers pools.
There are four pools used:
* `:federation` for the federation jobs.
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
* `:media` for rich media, media proxy
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
* `:default` for other requests
For each pool, the options are:
* `:size` - how much workers the pool can hold
* `:timeout` - timeout while `gun` will wait for response
* `:max_overflow` - additional workers if pool is under load
## Captcha
### Pleroma.Captcha
* `enabled`: Whether the captcha should be shown on registration.
* `method`: The method/service to use for captcha.
* `seconds_valid`: The time in seconds for which the captcha is valid.
### Captcha providers
#### Pleroma.Captcha.Native
A built-in captcha provider. Enabled by default.
#### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer.
* `endpoint`: the Kocaptcha endpoint to use.
## Uploads
### Pleroma.Upload
* `uploader`: Which one of the [uploaders](#uploaders) to use.
* `filters`: List of [upload filters](#upload-filters) to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
!!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
### Uploaders
#### Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory.
#### Pleroma.Uploaders.S3
* `bucket`: S3 bucket name.
* `bucket_namespace`: S3 bucket namespace.
* `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com")
* `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
### Upload filters
#### Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
#### Pleroma.Upload.Filter.Dedupe
No specific configuration.
#### Pleroma.Upload.Filter.AnonymizeFilename
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
`Pleroma.Upload.Filter.Dedupe` before AnonymizeFilename.
* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`.
## Email
### Pleroma.Emails.Mailer
* `adapter`: one of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters), or `Swoosh.Adapters.Local` for in-memory mailbox.
* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.
* `enabled`: Allows enable/disable send emails. Default: `false`.
An example for Sendgrid adapter:
```elixir
config :pleroma, Pleroma.Emails.Mailer,
enabled: true,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "YOUR_API_KEY"
```
An example for SMTP adapter:
```elixir
config :pleroma, Pleroma.Emails.Mailer,
enabled: true,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.gmail.com",
username: "YOUR_USERNAME@gmail.com",
password: "YOUR_SMTP_PASSWORD",
port: 465,
ssl: true,
auth: :always
```
### :email_notifications
Email notifications settings.
- digest - emails of "what you've missed" for users who have been
inactive for a while.
- active: globally enable or disable digest emails
- schedule: When to send digest email, in [crontab format](https://en.wikipedia.org/wiki/Cron).
"0 0 * * 0" is the default, meaning "once a week at midnight on Sunday morning"
- interval: Minimum interval between digest emails to one user
- inactivity_threshold: Minimum user inactivity threshold
### Pleroma.Emails.UserEmail
- `:logo` - a path to a custom logo. Set it to `nil` to use the default Pleroma logo.
- `:styling` - a map with color settings for email templates.
### Pleroma.Emails.NewUsersDigestEmail
- `:enabled` - a boolean, enables new users admin digest email when `true`. Defaults to `false`.
## Background jobs
### Oban
[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
* `repo` - app's Ecto repo (`Pleroma.Repo`)
* `verbose` - logs verbosity
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
* `queues` - job queues (see below)
* `crontab` - periodic jobs, see [`Oban.Cron`](#obancron)
Pleroma has the following queues:
* `activity_expiration` - Activity expiration
* `federator_outgoing` - Outgoing federation
* `federator_incoming` - Incoming federation
* `mailer` - Email sender, see [`Pleroma.Emails.Mailer`](#pleromaemailsmailer)
* `transmogrifier` - Transmogrifier
* `web_push` - Web push notifications
* `scheduled_activities` - Scheduled activities, see [`Pleroma.ScheduledActivity`](#pleromascheduledactivity)
#### Oban.Cron
Pleroma has these periodic job workers:
`Pleroma.Workers.Cron.ClearOauthTokenWorker` - a job worker to cleanup expired oauth tokens.
Example:
```elixir
config :pleroma, Oban,
repo: Pleroma.Repo,
verbose: false,
prune: {:maxlen, 1500},
queues: [
federator_incoming: 50,
federator_outgoing: 50
],
crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}
]
```
This config contains two queues: `federator_incoming` and `federator_outgoing`. Both have the number of max concurrent jobs set to `50`.
#### Migrating `pleroma_job_queue` settings
`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).
### :workers
Includes custom worker options not interpretable directly by `Oban`.
* `retries` — keyword lists where keys are `Oban` queues (see above) and values are numbers of max attempts for failed jobs.
Example:
```elixir
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
]
```
#### Migrating `Pleroma.Web.Federator.RetryQueue` settings
* `max_retries` is replaced with `config :pleroma, :workers, retries: [federator_outgoing: 5]`
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
* deprecated options: `max_jobs`, `initial_timeout`
### Pleroma.Scheduler
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
Example:
```elixir
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
timezone: :utc,
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
```
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
## :web_push_encryption, :vapid_details
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
* ``subject``: a mailto link for the administrative contact. It’s best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise can’t respond, someone else on the list can.
* ``public_key``: VAPID public key
* ``private_key``: VAPID private key
## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed:
```elixir
config :logger,
backends: [{ExSyslogger, :ex_syslogger}]
config :logger, :ex_syslogger,
level: :warn
```
Another example, keeping console output and adding the pid to syslog output:
```elixir
config :logger,
backends: [:console, {ExSyslogger, :ex_syslogger}]
config :logger, :ex_syslogger,
level: :warn,
option: [:pid, :ndelay]
```
See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_syslogger’s documentation](https://hexdocs.pm/ex_syslogger/)
An example of logging info to local syslog, but warn to a Slack channel:
```elixir
config :logger,
backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ],
level: :info
config :logger, :ex_syslogger,
level: :info,
ident: "pleroma",
format: "$metadata[$level] $message"
config :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-API-KEY-HERE"
```
See the [Quack Github](https://github.com/azohra/quack) for more details
## Database options
### RUM indexing for full text search
!!! warning
It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
* `rum_enabled`: If RUM indexes should be used. Defaults to `false`.
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. While they may eventually be mainlined, for now they have to be installed as a PostgreSQL extension from https://github.com/postgrespro/rum.
Their advantage over the standard GIN indexes is that they allow efficient ordering of search results by timestamp, which makes search queries a lot faster on larger servers, by one or two orders of magnitude. They take up around 3 times as much space as GIN indexes.
To enable them, both the `rum_enabled` flag has to be set and the following special migration has to be run:
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
This will probably take a long time.
## Alternative client protocols
### BBS / SSH access
To enable simple command line interface accessible over ssh, add a setting like this to your configuration file:
```exs
app_dir = File.cwd!
priv_dir = Path.join([app_dir, "priv/ssh_keys"])
config :esshd,
enabled: true,
priv_dir: priv_dir,
handler: "Pleroma.BBS.Handler",
port: 10_022,
password_authenticator: "Pleroma.BBS.Authenticator"
```
Feel free to adjust the priv_dir and port number. Then you will have to create the key for the keys (in the example `priv/ssh_keys`) and create the host keys with `ssh-keygen -m PEM -N "" -b 2048 -t rsa -f ssh_host_rsa_key`. After restarting, you should be able to connect to your Pleroma instance with `ssh username@server -p $PORT`
### :gopher
* `enabled`: Enables the gopher interface
* `ip`: IP address to bind to
* `port`: Port to bind to
* `dstport`: Port advertised in urls (optional, defaults to `port`)
## Authentication
### :admin_token
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter or `x-admin-token` HTTP header. Example:
```elixir
config :pleroma, :admin_token, "somerandomtoken"
```
You can then do
```shell
curl "http://localhost:4000/api/pleroma/admin/users/invites?admin_token=somerandomtoken"
```
or
```shell
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
```
### :auth
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication.
Authentication / authorization settings.
* `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`.
* `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.
* `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by `OAUTH_CONSUMER_STRATEGIES` environment variable. Each entry in this space-delimited string should be of format `<strategy>` or `<strategy>:<dependency>` (e.g. `twitter` or `keycloak:ueberauth_keycloak_strategy` in case dependency is named differently than `ueberauth_<strategy>`).
### Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication.
### :ldap
Use LDAP for user authentication. When a user logs in to the Pleroma
instance, the name and password will be verified by trying to authenticate
(bind) to an LDAP server. If a user exists in the LDAP directory but there
is no account with the same name yet on the Pleroma instance then a new
Pleroma account will be created with the same name as the LDAP user name.
* `enabled`: enables LDAP authentication
* `host`: LDAP server hostname
* `port`: LDAP port, e.g. 389 or 636
* `ssl`: true to use SSL, usually implies the port 636
* `sslopts`: additional SSL options
* `tls`: true to start TLS, usually implies the port 389
* `tlsopts`: additional TLS options
* `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
### OAuth consumer mode
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies).
!!! note
Each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies.
!!! note
Each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies.
!!! note
Make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you have this feature enabled. OAuth consumer mode will not work with `"SameSite=Strict"`
* For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback
* For Facebook, [register an app](https://developers.facebook.com/apps), configure callback URL to https://<your_host>/oauth/facebook/callback, enable Facebook Login service at https://developers.facebook.com/apps/<app_id>/fb-login/settings/
* For Google, [register an app](https://console.developers.google.com), configure callback URL to https://<your_host>/oauth/google/callback
* For Microsoft, [register an app](https://portal.azure.com), configure callback URL to https://<your_host>/oauth/microsoft/callback
Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`,
per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables:
```elixir
# Twitter
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"),
consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET")
# Facebook
config :ueberauth, Ueberauth.Strategy.Facebook.OAuth,
client_id: System.get_env("FACEBOOK_APP_ID"),
client_secret: System.get_env("FACEBOOK_APP_SECRET"),
redirect_uri: System.get_env("FACEBOOK_REDIRECT_URI")
# Google
config :ueberauth, Ueberauth.Strategy.Google.OAuth,
client_id: System.get_env("GOOGLE_CLIENT_ID"),
client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
redirect_uri: System.get_env("GOOGLE_REDIRECT_URI")
# Microsoft
config :ueberauth, Ueberauth.Strategy.Microsoft.OAuth,
client_id: System.get_env("MICROSOFT_CLIENT_ID"),
client_secret: System.get_env("MICROSOFT_CLIENT_SECRET")
config :ueberauth, Ueberauth,
providers: [
microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]}
]
# Keycloak
# Note: make sure to add `keycloak:ueberauth_keycloak_strategy` entry to `OAUTH_CONSUMER_STRATEGIES` environment variable
keycloak_url = "https://publicly-reachable-keycloak-instance.org:8080"
config :ueberauth, Ueberauth.Strategy.Keycloak.OAuth,
client_id: System.get_env("KEYCLOAK_CLIENT_ID"),
client_secret: System.get_env("KEYCLOAK_CLIENT_SECRET"),
site: keycloak_url,
authorize_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/auth",
token_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/token",
userinfo_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/userinfo",
token_method: :post
config :ueberauth, Ueberauth,
providers: [
keycloak: {Ueberauth.Strategy.Keycloak, [uid_field: :email]}
]
```
### OAuth 2.0 provider - :oauth2
Configure OAuth 2 provider capabilities:
* `token_expires_in` - The lifetime in seconds of the access token.
* `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`. Interval settings sets in configuration periodic jobs [`Oban.Cron`](#obancron)
## Link parsing
### :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL.
### :auto_linker
Configuration for the `auto_linker` library:
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear.
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear.
* `new_window: true` - set to false to remove `target='_blank'` attribute.
* `scheme: false` - Set to true to link urls with schema `http://google.com`.
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`.
* `strip_prefix: true` - Strip the scheme prefix.
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.).
Example:
```elixir
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
```
## Custom Runtime Modules (`:modules`)
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
## :configurable_from_database
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
## Restrict entities access for unauthenticated users
### :restrict_unauthenticated
Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
* `timelines` - public and federated timelines
* `local` - public timeline
* `federated`
* `profiles` - user profiles
* `local`
* `remote`
* `activities` - statuses
* `local`
- * `remote`
\ No newline at end of file
+ * `remote`
+
+## Pleroma.Web.ApiSpec.CastAndValidate
+
+* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 99358ddaf..2c343eb22 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,2172 +1,2170 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
require Logger
@type t :: %__MODULE__{}
@type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
# AP ID user relationships (blocks, mutes etc.)
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [
block: [
blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
],
mute: [
muter_mutes: :muted_users,
mutee_mutes: :muter_users
],
reblog_mute: [
reblog_muter_mutes: :reblog_muted_users,
reblog_mutee_mutes: :reblog_muter_users
],
notification_mute: [
notification_muter_mutes: :notification_muted_users,
notification_mutee_mutes: :notification_muter_users
],
# Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
inverse_subscription: [
subscribee_subscriptions: :subscriber_users,
subscriber_subscriptions: :subscribee_users
]
]
schema "users" do
field(:bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
- field(:magic_key, :string, default: nil)
field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
embeds_one(
:notification_settings,
Pleroma.User.NotificationSetting,
on_replace: :update
)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
has_many(:deliveries, Delivery)
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do
# Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
# :notification_muter_mutes, :subscribee_subscriptions
has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
# :notification_mutee_mutes, :subscriber_subscriptions
has_many(incoming_relation, UserRelationship,
foreign_key: :target_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
# :notification_muted_users, :subscriber_users
has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
# :notification_muter_users, :subscribee_users
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
# `:blocks` is deprecated (replaced with `blocked_users` relation)
field(:blocks, {:array, :string}, default: [])
# `:mutes` is deprecated (replaced with `muted_users` relation)
field(:mutes, {:array, :string}, default: [])
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
timestamps()
end
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do
# `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
# `def subscriber_users/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target))
if restrict_deactivated? do
restrict_deactivated(target_users_query)
else
target_users_query
end
end
# `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
# `def notification_muted_users/2`, `def subscriber_users/2`
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> Repo.all()
end
# `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
# `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> select([u], u.ap_id)
|> Repo.all()
end
end
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account"
@spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{confirmation_pending: true}) do
case Config.get([:instance, :account_activation_required]) do
true -> :confirmation_pending
_ -> :active
end
end
def account_status(%User{}), do: :active
@spec visible_for?(User.t(), User.t() | nil) :: boolean()
def visible_for?(user, for_user \\ nil)
def visible_for?(%User{invisible: true}, _), do: false
def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
def visible_for?(%User{local: local} = user, nil) do
cfg_key =
if local,
do: :local,
else: :remote
if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
do: false,
else: account_status(user) == :active
end
def visible_for?(%User{} = user, for_user) do
account_status(user) == :active || superuser?(for_user)
end
def visible_for?(_, _), do: false
@spec superuser?(User.t()) :: boolean()
def superuser?(%User{local: true, is_admin: true}), do: true
def superuser?(%User{local: true, is_moderator: true}), do: true
def superuser?(_), do: false
@spec invisible?(User.t()) :: boolean()
def invisible?(%User{invisible: true}), do: true
def invisible?(_), do: false
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
end
end
def banner_url(user, options \\ []) do
case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
# Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@spec ap_following(User.t()) :: String.t()
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true)
end
defdelegate following_count(user), to: FollowingRelationship
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
else
params
end
end
defp truncate_if_exists(params, key, max_length) do
if Map.has_key?(params, key) and is_binary(params[key]) do
{value, _chopped} = String.split_at(params[key], max_length)
Map.put(params, key, value)
else
params
end
end
defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
defp fix_follower_address(%{nickname: nickname} = params),
do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
name =
case params[:name] do
name when is_binary(name) and byte_size(name) > 0 -> name
_ -> params[:nickname]
end
params =
params
|> Map.put(:name, name)
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
|> fix_follower_address()
struct
|> cast(
params,
[
:bio,
:name,
:emoji,
:ap_id,
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
:locked,
:last_refreshed_at,
- :magic_key,
:uri,
:follower_address,
:following_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:discoverable,
:invisible,
:actor_type,
:also_known_as
]
)
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
end
def update_changeset(struct, params \\ %{}) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
struct
|> cast(
params,
[
:bio,
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:locked,
:no_rich_text,
:default_scope,
:banner,
:hide_follows,
:hide_followers,
:hide_followers_count,
:hide_follows_count,
:hide_favorites,
:allow_following_move,
:background,
:show_role,
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store,
:discoverable,
:actor_type,
:also_known_as
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> validate_fields(false)
end
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
|> put_change(:fields, fields)
else
changeset
end
end
defp parse_fields(value) do
value
|> Formatter.linkify(mentions_format: :full)
|> elem(0)
end
defp put_emoji(changeset) do
bio = get_change(changeset, :bio)
name = get_change(changeset, :name)
if bio || name do
emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
put_change(changeset, :emoji, emoji)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
_ -> changeset
end
else
changeset
end
end
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
{:ok, object.data}
end
end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
|> cast(params, [:email])
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
end
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
if params["password"] do
reset_password(user, changeset, params)
else
User.update_and_set_cache(changeset)
end
end
def password_update_changeset(struct, params) do
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash()
|> put_change(:password_reset_pending, false)
end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
def reset_password(%User{id: user_id} = user, struct, params) do
multi =
Multi.new()
|> Multi.update(:user, password_update_changeset(struct, params))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
case Repo.transaction(multi) do
{:ok, %{user: user} = _} -> set_cache(user)
{:error, _, changeset, _} -> {:error, changeset}
end
end
def update_password_reset_pending(user, value) do
user
|> change()
|> put_change(:password_reset_pending, value)
|> update_and_set_cache()
end
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user), do: update_password_reset_pending(user, true)
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
Pleroma.Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
if Pleroma.Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
end
end
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset
|> put_change(:follower_address, followers)
end
defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
follow_all(user, autofollowed_users)
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
def try_send_confirmation_email(%User{} = user) do
if user.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def try_send_confirmation_email(users) do
Enum.each(users, &try_send_confirmation_email/1)
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower)
end
defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
cond do
followed.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
FollowingRelationship.follow(follower, followed, state)
{:ok, _} = update_follower_count(followed)
follower
|> update_following_count()
|> set_cache()
end
end
def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"}
end
def unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed)
{:ok, follower} =
follower
|> update_following_count()
|> set_cache()
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
nil ->
{:error, "Not subscribed!"}
end
end
defdelegate following?(follower, followed), to: FollowingRelationship
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
end
def get_follow_state(
%User{} = follower,
%User{} = following,
following_relationship
) do
case {following_relationship, following.local} do
{nil, false} ->
case Utils.fetch_latest_follow(follower, following) do
%Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
FollowingRelationship.state_to_enum(state)
_ ->
nil
end
{%{state: state}, _} ->
state
{nil, _} ->
nil
end
end
def locked?(%User{} = user) do
user.locked || false
end
def get_by_id(id) do
Repo.get_by(User, id: id)
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def get_all_by_ap_id(ap_ids) do
from(u in __MODULE__,
where: u.ap_id in ^ap_ids
)
|> Repo.all()
end
def get_all_by_ids(ids) do
from(u in __MODULE__, where: u.id in ^ids)
|> Repo.all()
end
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
domain = URI.parse(ap_id).host
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
get_cached_by_nickname(nickname)
end
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
def update_and_set_cache(struct, params) do
struct
|> update_changeset(params)
|> update_and_set_cache()
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
end
def get_user_friends_ap_ids(user) do
from(u in User.get_friends_query(user), select: u.ap_id)
|> Repo.all()
end
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
with {:ok, nil} <- Cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:user_cache, key, user) do
user
else
{:ok, user} -> user
nil -> nil
end
end
def get_cached_by_id(id) do
key = "id:#{id}"
ap_id =
Cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id}
else
{:ignore, ""}
end
end)
get_cached_by_ap_id(ap_id)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn ->
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
cond do
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)
restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
true ->
nil
end
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
Repo.get_by(User, nickname: local_nickname(nickname))
end
end
def get_by_email(email), do: Repo.get_by(User, email: email)
def get_by_nickname_or_email(nickname_or_email) do
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
{:ok, user}
else
_e -> {:error, "not found " <> nickname}
end
end
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, deactivated: false})
end
def get_followers_query(user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user, deactivated: false})
end
def get_friends_query(user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ap_ids(user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
defdelegate get_follow_requests(user), to: FollowingRelationship
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u], inc: [note_count: 1])
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def decrease_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u],
set: [
note_count: fragment("greatest(0, note_count - 1)")
]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def update_note_count(%User{} = user, note_count \\ nil) do
note_count =
note_count ||
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
|> Repo.one()
user
|> cast(%{note_count: note_count}, [:note_count])
|> update_and_set_cache()
end
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do
user
else
e ->
Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
user
end
end
def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
user
|> follow_information_changeset(info)
|> update_and_set_cache()
end
end
defp follow_information_changeset(user, params) do
user
|> cast(params, [
:hide_followers,
:hide_follows,
:follower_count,
:following_count,
:hide_followers_count,
:hide_follows_count
])
end
def update_follower_count(%User{} = user) do
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
follower_count_query =
User.Query.build(%{followers: user, deactivated: false})
|> select([u], %{count: count(u.id)})
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(follower_count_query))
|> update([u, s],
set: [follower_count: s.count]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
else
{:ok, maybe_fetch_follow_information(user)}
end
end
@spec update_following_count(User.t()) :: User.t()
def update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
maybe_fetch_follow_information(user)
else
user
end
end
def update_following_count(%User{local: true} = user) do
following_count = FollowingRelationship.following_count(user)
user
|> follow_information_changeset(%{following_count: following_count})
|> Repo.update!()
end
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: [unread_conversation_count: 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(user), do: {:ok, user}
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],
inc: [unread_conversation_count: 1]
)
|> 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(_, user), do: {:ok, user}
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
|> Repo.all()
end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|> Repo.all()
end
@spec mute(User.t(), User.t(), boolean()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
add_to_mutes(muter, mutee, notifications?)
end
def unmute(%User{} = muter, %User{} = mutee) do
remove_from_mutes(muter, mutee)
end
def subscribe(%User{} = subscriber, %User{} = target) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
else
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.create_inverse_subscription(target, subscriber)
end
end
def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
subscribe(subscriber, subscribee)
end
end
def unsubscribe(%User{} = unsubscriber, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.delete_inverse_subscription(target, unsubscriber)
end
def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
unsubscribe(unsubscriber, user)
end
end
def block(%User{} = blocker, %User{} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
{:ok, blocker, _} = unfollow(blocker, blocked)
blocker
else
blocker
end
# clear any requested follows as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
unsubscribe(blocked, blocker)
if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def block(%User{} = blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(%User{} = blocker, %User{} = blocked) do
remove_from_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def unblock(%User{} = blocker, %{ap_id: ap_id}) do
unblock(blocker, get_cached_by_ap_id(ap_id))
end
def mutes?(nil, _), do: false
def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
def mutes_user?(%User{} = user, %User{} = target) do
UserRelationship.mute_exists?(user, target)
end
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(%User{} = user, %User{} = target),
do: UserRelationship.notification_mute_exists?(user, target)
def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) ||
(blocks_domain?(user, target) and not User.following?(user, target))
end
def blocks_user?(%User{} = user, %User{} = target) do
UserRelationship.block_exists?(user, target)
end
def blocks_user?(_, _), do: false
def blocks_domain?(%User{} = user, %User{} = target) do
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
%{host: host} = URI.parse(target.ap_id)
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
end
def blocks_domain?(_, _), do: false
def subscribed_to?(%User{} = user, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.inverse_subscription_exists?(target, user)
end
def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
subscribed_to?(user, target)
end
end
@doc """
Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
"""
@spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relationships_ap_ids(_user, []), do: %{}
def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do
db_result =
user
|> assoc(:outgoing_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :target))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
|> group_by([user_rel, u], user_rel.relationship_type)
|> Repo.all()
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
Enum.into(
relationship_types,
%{},
fn rel_type -> {rel_type, db_result[rel_type] || []} end
)
end
def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
when is_list(relationship_types) do
user
|> assoc(:incoming_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :source))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> maybe_filter_on_ap_id(ap_ids)
|> select([user_rel, u], u.ap_id)
|> distinct(true)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [user_rel, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
def deactivate(user, status \\ true)
def deactivate(users, status) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: deactivate(user, status)
end)
end
def deactivate(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(fn follower ->
follower |> update_following_count() |> set_cache()
end)
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&update_follower_count/1)
{:ok, user}
end
end
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
|> cast_embed(:notification_settings)
|> validate_required([:notification_settings])
|> update_and_set_cache()
end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user)
if user.local do
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
else
invalidate_cache(user)
Repo.delete(user)
end
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, _user_block} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
external: true,
active: true,
order_by: :id
})
end
@spec external_users(keyword()) :: [User.t()]
def external_users(opts \\ []) do
query =
external_users_query()
|> select([u], struct(u, [:id, :ap_id]))
query =
if opts[:max_id],
do: where(query, [u], u.id > ^opts[:max_id]),
else: query
query =
if opts[:limit],
do: limit(query, ^opts[:limit]),
else: query
Repo.all(query)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
BackgroundWorker.enqueue("blocks_import", %{
"blocker_id" => blocker.id,
"blocked_identifiers" => blocked_identifiers
})
end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
BackgroundWorker.enqueue("follow_import", %{
"follower_id" => follower.id,
"followed_identifiers" => followed_identifiers
})
end
def delete_user_activities(%User{ap_id: ap_id}) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
activity
|> Object.normalize()
|> ActivityPub.delete()
end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end
defp delete_activity(_activity), do: "Doing nothing"
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !needs_update?(user) do
{:ok, user}
else
fetch_by_ap_id(ap_id)
end
end
@doc """
Creates an internal service actor by URI if missing.
Optionally takes nickname for addressing.
"""
@spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
def get_or_create_service_actor_by_ap_id(uri, nickname) do
{_, user} =
case get_cached_by_ap_id(uri) do
nil ->
with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
{:error, nil}
end
%User{invisible: false} = user ->
set_invisible(user)
user ->
{:ok, user}
end
user
end
@spec set_invisible(User.t()) :: {:ok, User.t()}
defp set_invisible(user) do
user
|> change(%{invisible: true})
|> update_and_set_cache()
end
@spec create_service_actor(String.t(), String.t()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_service_actor(uri, nickname) do
%User{
invisible: true,
local: true,
ap_id: uri,
nickname: nickname,
follower_address: uri <> "/followers"
}
|> change
|> unique_constraint(:nickname)
|> Repo.insert()
|> set_cache()
end
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
_ -> :error
end
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(_), do: ""
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio
|> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
end)
end
def tag(nickname, tags) when is_binary(nickname),
do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
def untag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
end)
end
def untag(nickname, tags) when is_binary(nickname),
do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
defp update_tags(%User{} = user, new_tags) do
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
|> update_and_set_cache()
updated_user
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
end
end
def local_nickname(nickname_or_mention) do
nickname_or_mention
|> full_nickname()
|> String.split("@")
|> hd()
end
def full_nickname(nickname_or_mention),
do: String.trim_leading(nickname_or_mention, "@")
def error_user(ap_id) do
%User{
name: ap_id,
ap_id: ap_id,
nickname: "erroruser@example.com",
inserted_at: NaiveDateTime.utc_now()
}
end
@spec all_superusers() :: [User.t()]
def all_superusers do
User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all()
end
def muting_reblogs?(%User{} = user, %User{} = target) do
UserRelationship.reblog_mute_exists?(user, target)
end
def showing_reblogs?(%User{} = user, %User{} = target) do
not muting_reblogs?(user, target)
end
@doc """
The function returns a query to get users with no activity for given interval of days.
Inactive users are those who didn't read any notification, or had any activity where
the user is the activity's actor, during `inactivity_threshold` days.
Deactivated users will not appear in this list.
## Examples
iex> Pleroma.User.list_inactive_users()
%Ecto.Query{}
"""
@spec list_inactive_users_query(integer()) :: Ecto.Query.t()
def list_inactive_users_query(inactivity_threshold \\ 7) do
negative_inactivity_threshold = -inactivity_threshold
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
# Subqueries are not supported in `where` clauses, join gets too complicated.
has_read_notifications =
from(n in Pleroma.Notification,
where: n.seen == true,
group_by: n.id,
having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
select: n.user_id
)
|> Pleroma.Repo.all()
from(u in Pleroma.User,
left_join: a in Pleroma.Activity,
on: u.ap_id == a.actor,
where: not is_nil(u.nickname),
where: u.deactivated != ^true,
where: u.id not in ^has_read_notifications,
group_by: u.id,
having:
max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
is_nil(max(a.inserted_at))
)
end
@doc """
Enable or disable email notifications for user
## Examples
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
Pleroma.User{email_notifications: %{"digest" => true}}
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
Pleroma.User{email_notifications: %{"digest" => false}}
"""
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
User.update_email_notifications(user, %{type => status})
end
@doc """
Set `last_digest_emailed_at` value for the user to current time
"""
@spec touch_last_digest_emailed_at(t()) :: t()
def touch_last_digest_emailed_at(user) do
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
{:ok, updated_user} =
user
|> change(%{last_digest_emailed_at: now})
|> update_and_set_cache()
updated_user
end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
user
|> confirmation_changeset(need_confirmation: !user.confirmation_pending)
|> update_and_set_cache()
end
@spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
def toggle_confirmation(users) do
Enum.map(users, &toggle_confirmation/1)
end
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot
end
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
config = Pleroma.Config.get([:assets, :mascots])
default_mascot = Pleroma.Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
"id" => "default-mascot",
"url" => mascot[:url],
"preview_url" => mascot[:url],
"pleroma" => %{
"mime_type" => mascot[:mime_type]
}
}
end
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
user
|> cast(%{keys: pem}, [:keys])
|> validate_required([:keys])
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defdelegate search(query, opts \\ []), to: User.Search
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
end
defp put_password_hash(changeset), do: changeset
def is_internal_user?(%User{nickname: nil}), do: true
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
def is_internal_user?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
def get_delivered_users_by_object_id(object_id) do
from(u in User,
inner_join: delivery in assoc(u, :deliveries),
where: delivery.object_id == ^object_id
)
|> Repo.all()
end
def change_email(user, email) do
user
|> cast(%{email: email}, [:email])
|> validate_required([:email])
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, deactivated) do
user
|> cast(%{deactivated: deactivated}, [:deactivated])
|> update_and_set_cache()
end
def update_banner(user, banner) do
user
|> cast(%{banner: banner}, [:banner])
|> update_and_set_cache()
end
def update_background(user, background) do
user
|> cast(%{background: background}, [:background])
|> update_and_set_cache()
end
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
moderator: is_moderator
}
end
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
|> validate_change(:fields, fn :fields, fields ->
if Enum.all?(fields, &valid_field?/1) do
[]
else
[fields: "invalid"]
end
end)
end
defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
%{"name" => name, "value" => value}
end
def admin_api_update(user, params) do
user
|> cast(params, [
:is_moderator,
:is_admin,
:show_role
])
|> update_and_set_cache()
end
@doc "Signs user out of all applications"
def global_sign_out(user) do
OAuth.Authorization.delete_user_authorizations(user)
OAuth.Token.delete_user_tokens(user)
end
def mascot_update(user, url) do
user
|> cast(%{mascot: url}, [:mascot])
|> validate_required([:mascot])
|> update_and_set_cache()
end
def mastodon_settings_update(user, settings) do
user
|> cast(%{settings: settings}, [:settings])
|> validate_required([:settings])
|> update_and_set_cache()
end
@spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
def confirmation_changeset(user, need_confirmation: need_confirmation?) do
params =
if need_confirmation? do
%{
confirmation_pending: true,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
}
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
cast(user, params, [:confirmation_pending, :confirmation_token])
end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
max: max_pinned_statuses,
message: "You have already pinned the maximum number of statuses"
)
else
change(user)
end
|> update_and_set_cache()
end
def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}
user
|> cast(params, [:pinned_activities])
|> update_and_set_cache()
end
def update_email_notifications(user, settings) do
email_notifications =
user.email_notifications
|> Map.merge(settings)
|> Map.take(["digest"])
params = %{email_notifications: email_notifications}
fields = [:email_notifications]
user
|> cast(params, fields)
|> validate_required(fields)
|> update_and_set_cache()
end
defp set_domain_blocks(user, domain_blocks) do
params = %{domain_blocks: domain_blocks}
user
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
|> update_and_set_cache()
end
def block_domain(user, domain_blocked) do
set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
end
def unblock_domain(user, domain_blocked) do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
UserRelationship.create_block(user, blocked)
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
UserRelationship.delete_block(user, blocked)
end
defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
{:ok, user_notification_mute} <-
(notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
{:ok, nil} do
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
end
defp remove_from_mutes(user, %User{} = muted_user) do
with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(user, muted_user) do
{:ok, [user_mute, user_notification_mute]}
end
end
def set_invisible(user, invisible) do
params = %{invisible: invisible}
user
|> cast(params, [:invisible])
|> validate_required([:invisible])
|> update_and_set_cache()
end
def sanitize_html(%User{} = user) do
sanitize_html(user, nil)
end
# User data that mastodon isn't filtering (treated as plaintext):
# - field name
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
user
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 1f4a09370..1c21d78af 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1640 +1,1653 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil -> true
user -> User.following?(user, actor)
end
end)
{recipients, to, cc}
end
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
false <- user.deactivated do
true
else
_e -> false
end
else
true
end
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
def increase_replies_count_if_reply(_create_data), do: :noop
def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Object.decrease_replies_count(reply_ap_id)
end
end
def decrease_replies_count_if_reply(_object), do: :noop
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create",
"actor" => actor
}) do
Object.increase_vote_count(reply_ap_id, name, actor)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
{:ok, activity} <-
Repo.insert(%Activity{
data: object,
local: local,
recipients: recipients,
actor: object["actor"]
}) do
{:ok, activity, meta}
end
end
@spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
})
# Splice in the child object if we have one.
activity =
if not is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
Notification.create_notifications(activity)
conversation = create_or_bump_conversation(activity, map["actor"])
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
id: "pleroma:fakeid"
}
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
end
end
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor),
Participation.mark_as_read(user, conversation) do
{:ok, conversation}
end
end
defp get_participations({:ok, conversation}) do
conversation
|> Repo.preload(:participations, force: true)
|> Map.get(:participations)
end
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Streamer.stream("participation", participations)
end
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
conversation = Repo.preload(conversation, :participations),
last_activity_id =
fetch_latest_activity_id_for_context(conversation.ap_id, %{
"user" => user,
"blocking_user" => user
}) do
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
def stream_out_participations(_, _), do: :noop
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete"] do
activity
|> Topics.get_activity_topics()
|> Streamer.stream(activity)
end
def stream_out(_activity) do
:noop
end
@spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def create(params, fake \\ false) do
with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
result
end
end
defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
quick_insert? = Config.get([:env]) == :benchmark
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:quick_insert, true, activity} ->
{:ok, activity}
{:fake, true, activity} ->
{:ok, activity}
{:error, message} ->
Repo.rollback(message)
end
end
@spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with listen_data <-
make_listen_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(listen_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec accept(map()) :: {:ok, Activity.t()} | {:error, any()}
def accept(params) do
accept_or_reject("Accept", params)
end
@spec reject(map()) :: {:ok, Activity.t()} | {:error, any()}
def reject(params) do
accept_or_reject("Reject", params)
end
@spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
local = Map.get(params, :local, true)
activity_id = Map.get(params, :activity_id, nil)
with data <-
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec update(map()) :: {:ok, Activity.t()} | {:error, any()}
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false)
activity_id = params[:activity_id]
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def react_with_emoji(user, object, emoji, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
result
end
end
defp do_react_with_emoji(user, object, emoji, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
{:ok, activity} <- insert(reaction_data, local),
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
false -> {:error, false}
{:error, error} -> Repo.rollback(error)
end
end
@spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def unreact_with_emoji(user, reaction_id, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
result
end
end
defp do_unreact_with_emoji(user, reaction_id, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
user_ap_id <- user.ap_id,
%Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
object <- Object.normalize(reaction_activity),
unreact_data <- make_undo_data(user, reaction_activity, activity_id),
{:ok, activity} <- insert(unreact_data, local),
{:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
result
end
end
defp do_unlike(actor, object, activity_id, local) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with {:ok, result} <-
Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
result
end
end
defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public),
object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
false -> {:error, false}
{:error, error} -> Repo.rollback(error)
end
end
@spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with {:ok, result} <-
Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
result
end
end
defp do_unannounce(actor, object, activity_id, local) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
result
end
end
defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
result
end
end
defp do_unfollow(follower, followed, activity_id, local) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
def delete(entity, options \\ []) do
with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
result
end
end
defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
local = Keyword.get(options, :local, true)
activity_id = Keyword.get(options, :activity_id, nil)
actor = Keyword.get(options, :actor, actor)
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with create_activity <- Activity.get_create_by_object_ap_id(id),
data <-
%{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => create_activity && create_activity.id
}
|> maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local, false),
{:ok, object, _create_activity} <- Object.delete(object),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} ->
Repo.rollback(error)
end
end
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
activity =
ap_id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Delete")
|> Repo.one()
{:ok, activity}
end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_block(blocker, blocked, activity_id, local) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} | nil
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_unblock(blocker, blocked, activity_id, local) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(
%{
actor: actor,
context: _context,
account: account,
statuses: statuses,
content: content
} = params
) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
:ok <- maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
end
end
@spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def move(%User{} = origin, %User{} = target, local \\ true) do
params = %{
"type" => "Move",
"actor" => origin.ap_id,
"object" => origin.ap_id,
"target" => target.ap_id
}
with true <- origin.ap_id in target.also_known_as,
{:ok, activity} <- insert(params, local) do
maybe_federate(activity)
BackgroundWorker.enqueue("move_following", %{
"origin_id" => origin.id,
"target_id" => target.id
})
{:ok, activity}
else
false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
err -> err
end
end
def fetch_activities_for_context_query(context, opts) do
public = [Constants.as_public()]
recipients =
if opts["user"],
do: [opts["user"].ap_id | User.following(opts["user"])] ++ public,
else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
)
|> exclude_poll_votes(opts)
|> exclude_id(opts)
|> order_by([activity], desc: activity.id)
end
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])
[Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination)
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ?",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
defp restrict_thread_visibility(
query,
%{"user" => %User{skip_thread_containment: true}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params)
|> Enum.reverse()
end
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("pinned_activity_ids", user.pinned_activities)
params =
if User.blocks?(reading_user, user) do
params
else
params
|> Map.put("blocking_user", reading_user)
|> Map.put("muting_user", reading_user)
end
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params)
|> Enum.reverse()
end
def fetch_statuses(reading_user, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params, :offset)
|> Enum.reverse()
end
defp user_activities_recipients(%{"godmode" => true}) do
[]
end
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
[Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
else
[Constants.as_public()]
end
end
defp restrict_since(query, %{"since_id" => ""}), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end
defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{"type" => type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "self"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? = ANY(?)",
object.data,
^user.ap_id,
activity.recipients
)
)
end
defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "following"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
object.data,
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
activity.recipients,
activity.actor,
activity.actor,
^user.ap_id
)
)
end
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
unless opts["skip_preload"] do
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
else
query
end
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || []
following_ap_ids = User.get_friends_ap_ids(user)
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from(
[activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocked_ap_ids
),
where:
fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
activity.actor,
^domain_blocks,
activity.actor,
^following_ap_ids
),
where:
fragment(
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
o.data,
^domain_blocks,
o.data,
^following_ap_ids
)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^[Constants.as_public()]
)
)
end
# TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
# the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
# and `restrict_muted/2`
defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
when pinned in [true, "true", "1"] do
from(activity in query, where: activity.id in ^ids)
end
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp restrict_instance(query, %{"instance" => instance}) do
users =
from(
u in User,
select: u.ap_id,
where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
)
|> Repo.all()
from(activity in query, where: activity.actor in ^users)
end
defp restrict_instance(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
query
|> Activity.with_preloaded_report_notes()
end
defp maybe_preload_report_notes(query, _), do: query
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts["muting_user"]
ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relationships =
ap_id_relationships ++
if opts["blocking_user"] && opts["blocking_user"] == source_user do
[:block]
else
[]
end
preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
restrict_muted_reblogs_opts =
Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
end
def fetch_activities_query(recipients, opts \\ %{}) do
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
fetch_activities_query_ap_ids_ops(opts)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_replies(opts)
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
|> exclude_visibility(opts)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
@spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Like")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([_like, object, activity], %{activity | object: object})
|> order_by([like, _, _], desc: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{"skip_order" => true}),
pagination,
:object_activity
)
end
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
when is_list(list_memberships) and length(list_memberships) > 0 do
Enum.map(activities, fn
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
activity
end
activity ->
activity
end)
end
defp maybe_update_cc(activities, _, _), do: activities
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
^Constants.as_public() in activity.recipients)
)
end
def fetch_activities_bounded(
recipients,
recipients_with_public,
opts \\ %{},
pagination \\ :keyset
) do
fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
obj_data =
if opts[:actor] do
Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data})
end
end
@spec get_actor_url(any()) :: binary() | nil
defp get_actor_url(url) when is_binary(url), do: url
defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
defp get_actor_url(url) when is_list(url) do
url
|> List.first()
|> get_actor_url()
end
defp get_actor_url(_url), do: nil
defp object_to_user_data(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
emojis =
data
|> Map.get("tag", [])
|> Enum.filter(fn
%{"type" => "Emoji"} -> true
_ -> false
end)
|> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
Map.put(acc, String.trim(name, ":"), url)
end)
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
else
nil
end
shared_inbox =
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
data["endpoints"]["sharedInbox"]
else
nil
end
user_data = %{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
ap_enabled: true,
banner: banner,
fields: fields,
emoji: emojis,
locked: locked,
discoverable: discoverable,
invisible: invisible,
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
bio: data["summary"],
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox
}
# nickname can be nil because of virtual actors
user_data =
if data["preferredUsername"] do
Map.put(
user_data,
:nickname,
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
)
else
Map.put(user_data, :nickname, nil)
end
{:ok, user_data}
end
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
hide_follows: hide_follows,
follower_count: normalize_counter(followers_data["totalItems"]),
following_count: normalize_counter(following_data["totalItems"]),
hide_followers: hide_followers
}}
else
{:error, _} = e -> e
e -> {:error, e}
end
end
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
- defp maybe_update_follow_information(data) do
+ def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
- {:ok, info} <- fetch_follow_information_for_user(data) do
- info = Map.merge(data[:info] || %{}, info)
- Map.put(data, :info, info)
+ {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
+ {_, true} <-
+ {:collections_available,
+ !!(user_data[:following_address] && user_data[:follower_address])},
+ {:ok, info} <-
+ fetch_follow_information_for_user(user_data) do
+ info = Map.merge(user_data[:info] || %{}, info)
+
+ user_data
+ |> Map.put(:info, info)
else
+ {:user_type_check, false} ->
+ user_data
+
+ {:collections_available, false} ->
+ user_data
+
{:enabled, false} ->
- data
+ user_data
e ->
Logger.error(
- "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
+ "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
- data
+ user_data
end
end
defp collection_private(%{"first" => %{"type" => type}})
when type in ["CollectionPage", "OrderedCollectionPage"],
do: {:ok, false}
defp collection_private(%{"first" => first}) do
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(first) do
{:ok, false}
else
{:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
{:error, _} = e -> e
e -> {:error, e}
end
end
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do
{:ok, data}
else
e -> {:error, e}
end
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data),
data <- maybe_update_follow_information(data) do
{:ok, data}
else
{:error, "Object has been deleted"} = e ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
e ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
end
end
def make_user_from_ap_id(ap_id) do
user = User.get_cached_by_ap_id(ap_id)
if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
if user do
user
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
else
data
|> User.remote_user_changeset()
|> Repo.insert()
|> User.set_cache()
end
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
def fetch_direct_messages_query do
Activity
|> restrict_type(%{"type" => "Create"})
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
index 49546ceaa..1bce739bd 100644
--- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -1,57 +1,99 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
+ alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
- field(:to, {:array, :string})
- field(:cc, {:array, :string})
+ field(:to, {:array, :string}, default: [])
+ field(:cc, {:array, :string}, default: [])
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
- |> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
+ |> changeset(data)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> fix_after_cast()
+ end
+
+ def fix_after_cast(cng) do
+ cng
+ |> fix_recipients()
+ |> fix_context()
+ end
+
+ def fix_context(cng) do
+ object = get_field(cng, :object)
+
+ with nil <- get_field(cng, :context),
+ %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
+ cng
+ |> put_change(:context, context)
+ else
+ _ ->
+ cng
+ end
+ end
+
+ def fix_recipients(cng) do
+ to = get_field(cng, :to)
+ cc = get_field(cng, :cc)
+ object = get_field(cng, :object)
+
+ with {[], []} <- {to, cc},
+ %Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
+ {:ok, actor} <- Types.ObjectID.cast(actor) do
+ cng
+ |> put_change(:to, [actor])
+ else
+ _ ->
+ cng
+ end
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_existing_like()
end
def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
cng
|> add_error(:actor, "already liked this object")
|> add_error(:object, "already liked by this actor")
else
cng
end
end
def validate_existing_like(cng), do: cng
end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 7ccee54c9..d5abb7567 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -1,42 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator
- @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
+ @spec common_pipeline(map(), keyword()) ::
+ {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
+ case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
+ {:ok, value} ->
+ value
+
+ {:error, e} ->
+ {:error, e}
+ end
+ end
+
+ def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
- {_, {:ok, %Activity{} = activity, meta}} <-
+ {_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
- {_, {:ok, %Activity{} = activity, meta}} <-
+ {_, {:ok, activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
{:mrf_object, {:reject, _}} -> {:ok, nil, meta}
e -> {:error, e}
end
end
- defp maybe_federate(activity, meta) do
+ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
+
+ defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do
Federator.publish(activity)
{:ok, :federated}
else
{:ok, :not_federated}
end
else
_e -> {:error, :badarg}
end
end
end
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 5981e7545..6a8f1af96 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -1,35 +1,30 @@
defmodule Pleroma.Web.ActivityPub.SideEffects do
@moduledoc """
This module looks at an inserted object and executes the side effects that it
implies. For example, a `Like` activity will increase the like count on the
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
# Tasks this handles:
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
- {:ok, result} =
- Pleroma.Repo.transaction(fn ->
- liked_object = Object.get_by_ap_id(object.data["object"])
- Utils.add_like_to_object(object, liked_object)
+ liked_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_like_to_object(object, liked_object)
- Notification.create_notifications(object)
+ Notification.create_notifications(object)
- {:ok, object, meta}
- end)
-
- result
+ {:ok, object, meta}
end
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index c966ec960..581e7040b 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,1340 +1,1291 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
alias Pleroma.Workers.TransmogrifierWorker
import Ecto.Query
require Logger
require Pleroma.Constants
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object, options \\ []) do
object
|> strip_internal_fields
|> fix_actor
|> fix_url
|> fix_attachments
|> fix_context
|> fix_in_reply_to(options)
|> fix_emoji
|> fix_tag
|> fix_content_map
|> fix_addressing
|> fix_summary
|> fix_type(options)
|> fix_content
end
def fix_summary(%{"summary" => nil} = object) do
Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
cond do
is_binary(map[field]) ->
Map.put(map, field, [map[field]])
is_nil(map[field]) ->
Map.put(map, field, [])
true ->
map
end
end
def fix_explicit_addressing(
%{"to" => to, "cc" => cc} = object,
explicit_mentions,
follower_collection
) do
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
|> Map.put("to", explicit_to)
|> Map.put("cc", final_cc)
end
def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do
explicit_mentions = Utils.determine_explicit_mentions(object)
%User{follower_address: follower_collection} =
object
|> Containment.get_actor()
|> User.get_cached_by_ap_id()
explicit_mentions =
explicit_mentions ++
[
Pleroma.Constants.as_public(),
follower_collection
]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
Pleroma.Constants.as_public() in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
Pleroma.Constants.as_public() in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do
{:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing()
|> fix_implicit_addressing(followers_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
else
object
end
end
def fix_in_reply_to(object, _options), do: object
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
object
|> Map.put("context", context)
|> Map.put("conversation", context)
end
defp add_if_present(map, _key, nil), do: map
defp add_if_present(map, key, value) do
Map.put(map, key, value)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
url =
cond do
is_list(data["url"]) -> List.first(data["url"])
is_map(data["url"]) -> data["url"]
true -> nil
end
media_type =
cond do
is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
is_binary(data["mediaType"]) -> data["mediaType"]
is_binary(data["mimeType"]) -> data["mimeType"]
true -> nil
end
href =
cond do
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
end
attachment_url =
%{"href" => href}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", data["type"])
|> add_if_present("name", data["name"])
end)
Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
object
|> Map.put("attachment", [attachment])
|> fix_attachments()
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
Map.put(object, "url", url["href"])
end
def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type in ["Video", "Audio"] and is_list(url) do
first_element = Enum.at(url, 0)
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
object
|> Map.put("attachment", [first_element])
|> Map.put("url", link_element["href"])
end
def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type != "Video" and is_list(url) do
first_element = Enum.at(url, 0)
url_string =
cond do
is_bitstring(first_element) -> first_element
is_map(first_element) -> first_element["href"] || ""
true -> ""
end
Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]
Map.put(object, "tag", combined)
end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
def fix_tag(object), do: object
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
Map.put(object, "content", content)
end
def fix_content_map(object), do: object
def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
with true <- Federator.allowed_thread_distance?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
_ -> object
end
end
def fix_type(object, _), do: object
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
when is_binary(content) do
html_content =
content
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|> Pleroma.HTML.filter_tags()
Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
end
defp fix_content(object), do: object
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
{:ok, activity}
else
_ -> {:error, nil}
end
end
defp mastodon_follow_hack(_, _), do: {:error, nil}
defp get_follow_activity(follow_object, followed) do
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
{:ok, activity}
else
# Can't find the activity. This might a Mastodon 2.3 "Accept"
{:activity, nil} ->
mastodon_follow_hack(follow_object, followed)
_ ->
{:error, nil}
end
end
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{"cc" => [account.ap_id]}
}
|> ActivityPub.flag()
end
end
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}, _options), do: :error
def handle_incoming(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
do: :error
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options
)
when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(object, options)
params = %{
to: data["to"],
object: object,
actor: user,
context: object["conversation"],
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"directMessage",
"id"
])
}
with {:ok, created_activity} <- ActivityPub.create(params) do
reply_depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(reply_depth) do
for reply_id <- replies(object) do
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
"id" => reply_id,
"depth" => reply_depth
})
end
end
{:ok, created_activity}
end
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
end
end
def handle_incoming(
%{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
options
) do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
reply_depth = (options[:depth] || 0) + 1
options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
to: data["to"],
object: object,
actor: user,
context: nil,
local: false,
published: data["published"],
additional: Map.take(data, ["cc", "id"])
}
ActivityPub.listen(params)
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options
) do
with %User{local: true} = followed <-
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
{:ok, _relationship} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:user_locked, true} ->
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
:noop
end
{:ok, activity}
else
_e ->
:error
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false,
activity_id: id
})
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, activity} <-
ActivityPub.reject(%{
to: follow_activity.data["to"],
type: "Reject",
actor: followed,
object: follow_activity.data["id"],
local: false,
activity_id: id
}) do
{:ok, activity}
else
_e -> :error
end
end
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
"laugh" => "😆",
"hmm" => "🤔",
"surprise" => "😮",
"congrats" => "🎉",
"angry" => "💢",
"confused" => "😥",
"rip" => "😇",
"pudding" => "🍮",
"star" => "⭐"
}
@doc "Rewrite misskey likes into EmojiReacts"
def handle_incoming(
%{
"type" => "Like",
"_misskey_reaction" => reaction
} = data,
options
) do
data
|> Map.put("type", "EmojiReact")
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|> handle_incoming(options)
end
def handle_incoming(%{"type" => "Like"} = data, _options) do
- with {_, {:ok, cast_data_sym}} <-
- {:casting_data,
- data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
- cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
- :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
- {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
- {_, {:ok, cast_data}} <-
- {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
- {_, {:ok, activity, _meta}} <-
- {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
+ with :ok <- ObjectValidator.fetch_actor_and_object(data),
+ {:ok, activity, _meta} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
e -> {:error, e}
end
end
def handle_incoming(
%{
"type" => "EmojiReact",
"object" => object_id,
"actor" => _actor,
"id" => id,
"content" => emoji
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <-
ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_embedded_obj_helper(object_id, actor),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
data,
_options
)
when object_type in [
"Person",
"Application",
"Service",
"Organization"
] do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
actor
|> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_id,
activity_id: data["id"]
})
else
e ->
Logger.error(e)
:error
end
end
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
_options
) do
object_id = Utils.get_ap_id(object_id)
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <-
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
{:ok, activity}
else
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
User.delete(user)
nil ->
:error
end
_e ->
:error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, activity, _} <-
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
activity_id: id,
local: false
) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data,
_options
) do
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
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(
%{
"type" => "Move",
"actor" => origin_actor,
"object" => origin_actor,
"target" => target_actor
},
_options
) do
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
true <- origin_actor in target_user.also_known_as do
ActivityPub.move(origin_user, target_user, false)
else
_e -> :error
end
end
def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
case Object.normalize(id, true, options) do
%Object{} = object -> {:ok, object}
_ -> nil
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
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
@doc """
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
Based on Mastodon's ActivityPub::NoteSerializer#replies.
"""
def set_replies(obj_data) do
replies_uris =
with limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
%Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
object
|> Object.self_replies()
|> select([o], fragment("?->>'id'", o.data))
|> limit(^limit)
|> Repo.all()
else
_ -> []
end
set_replies(obj_data, replies_uris)
end
defp set_replies(obj, []) do
obj
end
defp set_replies(obj, replies_uris) do
replies_collection = %{
"type" => "Collection",
"items" => replies_uris
}
Map.merge(obj, %{"replies" => replies_collection})
end
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
items
end
def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
items
end
def replies(_), do: []
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
|> set_sensitive
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> set_replies
|> strip_internal_fields
|> strip_internal_tags
|> set_type
end
# @doc
# """
# internal -> Mastodon
# """
def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize()
|> Map.get(:data)
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
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,
# because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_outgoing(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
|> strip_internal_fields
|> maybe_fix_object_url
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
with false <- String.starts_with?(object, "http"),
{:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
%{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
relative_object do
Map.put(data, "object", external_url)
else
{:fetch, e} ->
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
data
_ ->
data
end
end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn
# Expand internal representation tags into AS2 tags.
tag when is_binary(tag) ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
# Do not process tags which are already AS2 tag objects.
tag when is_map(tag) ->
tag
end)
Map.put(object, "tag", tags)
end
def add_mention_tags(object) do
{enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
potential_receivers = enabled_receivers ++ disabled_receivers
mentions = Enum.map(potential_receivers, &build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
def take_emoji_tags(%User{emoji: emoji}) do
emoji
|> Map.to_list()
|> Enum.map(&build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out = Enum.map(emoji, &build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
defp build_emoji_tag({name, url}) do
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
end
def prepare_attachments(object) do
attachments =
object
|> Map.get("attachment", [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{
"url" => href,
"mediaType" => media_type,
"name" => data["name"],
"type" => "Document"
}
end)
Map.put(object, "attachment", attachments)
end
def strip_internal_fields(object) do
Map.drop(object, Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
tags = Enum.filter(tags, fn x -> is_map(x) end)
Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
from(
a in Activity,
where: ^old_follower_address in a.recipients,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
|> Repo.update_all([])
end
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
{:ok, user} <- update_user(user, data) do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
else
%User{} = user -> {:ok, user}
e -> e
end
end
defp update_user(user, data) do
user
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
end
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
-
- defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
- do: {:ok, data}
-
- defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
- with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
- {:ok, Map.put(data, "context", context)}
- else
- _ ->
- {:error, :no_context}
- end
- end
-
- defp ensure_context_presence(_) do
- {:error, :no_context}
- end
-
- defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
- do: {:ok, data}
-
- defp ensure_recipients_presence(%{"object" => object} = data) do
- case Object.normalize(object) do
- %{data: %{"actor" => actor}} ->
- data =
- data
- |> Map.put("to", [actor])
- |> Map.put("cc", data["cc"] || [])
-
- {:ok, data}
-
- nil ->
- {:error, :no_object}
-
- _ ->
- {:error, :no_actor}
- end
- end
-
- defp ensure_recipients_presence(_) do
- {:error, :no_object}
- end
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 816c11e01..ac661e515 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -1,1171 +1,1181 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
@descriptions_json Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get, :show_user_credentials]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"], admin: true}
when action in [
:get_password_reset,
:force_password_reset,
:user_delete,
:users_create,
:user_toggle_activation,
:user_activate,
:user_deactivate,
:tag_users,
:untag_users,
:right_add,
:right_add_multiple,
:right_delete,
:right_delete_multiple,
:update_user_credentials
]
)
plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
plug(
OAuthScopesPlug,
%{scopes: ["write:invites"], admin: true}
when action in [:create_invite_token, :revoke_invite, :email_invite]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:reports"], admin: true}
when action in [:list_reports, :report_show]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true}
when action in [:reports_update, :report_notes_create, :report_notes_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
+ when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"], admin: true}
when action in [:status_update, :status_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
when action in [
:config_show,
:list_log,
:stats,
:relay_list,
:config_descriptions,
:need_reboot
]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action in [
:restart,
:config_update,
:resend_confirmation_email,
:confirm_email,
:oauth_app_create,
:oauth_app_list,
:oauth_app_update,
:oauth_app_delete,
:reload_emoji
]
)
action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "delete"
})
conn
|> json(nickname)
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.delete(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "delete"
})
conn
|> json(nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end
conn
|> json("ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end
conn
|> json("ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
changesets =
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
case Pleroma.Repo.transaction(changesets) do
{:ok, users} ->
res =
users
|> Map.values()
|> Enum.map(fn user ->
{:ok, user} = User.post_register_action(user)
user
end)
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
ModerationLog.insert_log(%{
actor: admin,
subjects: Map.values(users),
action: "create"
})
conn
|> json(res)
{:error, id, changeset, _} ->
res =
Enum.map(changesets.operations, fn
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
AccountView.render("create-error.json", %{changeset: changeset})
{_, {:changeset, current_changeset, _}} ->
AccountView.render("create-error.json", %{changeset: current_changeset})
end)
conn
|> put_status(:conflict)
|> json(res)
end
end
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_statuses(nil, %{
"instance" => instance,
"limit" => page_size,
"offset" => (page - 1) * page_size,
"exclude_reblogs" => !with_reblogs && "true"
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
{_, page_size} = page_params(params)
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
"godmode" => godmode,
"exclude_reblogs" => !with_reblogs && "true"
})
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
else
_ -> {:error, :not_found}
end
end
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.deactivated)
action = if user.deactivated, do: "activate", else: "deactivate"
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: action
})
conn
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, false)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "activate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, true)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "deactivate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "tag"
})
json_response(conn, :no_content, "")
end
end
def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.untag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "untag"
})
json_response(conn, :no_content, "")
end
end
def list_users(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
search_params = %{
query: params["query"],
page: page,
page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"]
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
{:ok, users, count} <- filter_service_users(users, count),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
defp filter_service_users(users, count) do
filtered_users = Enum.reject(users, &service_user?/1)
count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
{:ok, filtered_users, count}
end
defp service_user?(user) do
String.match?(user.ap_id, ~r/.*\/relay$/) or
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
end
@filters ~w(local external active deactivated is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Enum.map(&String.to_atom(&1))
|> Enum.into(%{}, &{&1, true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nicknames" => nicknames
})
when permission_group in ["moderator", "admin"] do
update = %{:"is_#{permission_group}" => true}
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
for u <- users, do: User.admin_api_update(u, update)
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, update)
end
def right_add_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_add(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nickname" => nickname
})
when permission_group in ["moderator", "admin"] do
fields = %{:"is_#{permission_group}" => true}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: [user],
permission: permission_group
})
json(conn, fields)
end
def right_add(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_get(conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
conn
|> json(%{
is_moderator: user.is_moderator,
is_admin: user.is_admin
})
end
def right_delete_multiple(
%{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
%{
"permission_group" => permission_group,
"nicknames" => nicknames
}
)
when permission_group in ["moderator", "admin"] do
with false <- Enum.member?(nicknames, admin_nickname) do
update = %{:"is_#{permission_group}" => false}
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
for u <- users, do: User.admin_api_update(u, update)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, update)
else
_ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
end
end
def right_delete_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_delete(
%{assigns: %{user: admin}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
fields = %{:"is_#{permission_group}" => false}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: [user],
permission: permission_group
})
json(conn, fields)
end
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
def relay_list(conn, _params) do
with {:ok, list} <- Relay.list() do
json(conn, %{relays: list})
else
_ ->
conn
|> put_status(500)
end
end
def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{
action: "relay_follow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.unfollow(target) do
ModerationLog.insert_log(%{
action: "relay_unfollow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
else
{:registrations_open, _} ->
errors(
conn,
{:error, "To send invites you need to set the `registrations_open` option to false."}
)
{:invites_enabled, _} ->
errors(
conn,
{:error, "To send invites you need to set the `invites_enabled` option to true."}
)
end
end
@doc "Create an account registration invite token"
def create_invite_token(conn, params) do
opts = %{}
opts =
if params["max_use"],
do: Map.put(opts, :max_use, params["max_use"]),
else: opts
opts =
if params["expires_at"],
do: Map.put(opts, :expires_at, params["expires_at"]),
else: opts
{:ok, invite} = UserInviteToken.create_invite(opts)
json(conn, AccountView.render("invite.json", %{invite: invite}))
end
@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()
conn
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end
@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end
@doc "Force password reset for a given user"
def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
Enum.each(users, &User.force_password_reset_async/1)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "force_password_reset"
})
json_response(conn, :no_content, "")
end
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin})
else
_ -> {:error, :not_found}
end
end
@doc "Updates a given user"
def update_user_credentials(
%{assigns: %{user: admin}} = conn,
%{"nickname" => nickname} = params
) do
with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
{:ok, _user} <-
User.update_as_admin(user, params) do
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "updated_users"
})
if params["password"] do
User.force_password_reset_async(user)
end
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "force_password_reset"
})
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
end
def list_reports(conn, params) do
{page, page_size} = page_params(params)
reports = Utils.get_reports(params, page, page_size)
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
end
def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
result =
reports
|> Enum.map(fn report ->
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: activity
})
activity
else
{:error, message} -> %{id: report["id"], error: message}
end
end)
case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end
end
def report_notes_create(%{assigns: %{user: user}} = conn, %{
"id" => report_id,
"content" => content
}) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
subject: Activity.get_by_id(report_id),
text: content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def report_notes_delete(%{assigns: %{user: user}} = conn, %{
"id" => note_id,
"report_id" => report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
text: note.content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
godmode = params["godmode"] == "true" || params["godmode"] == true
local_only = params["local_only"] == "true" || params["local_only"] == true
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_statuses(nil, %{
"godmode" => godmode,
"local_only" => local_only,
"limit" => page_size,
"offset" => (page - 1) * page_size,
"exclude_reblogs" => !with_reblogs && "true"
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
+ def status_show(conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", %{activity: activity})
+ else
+ _ -> errors(conn, {:error, :not_found})
+ end
+ end
+
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
visibility: params["visibility"]
})
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
ModerationLog.insert_log(%{
action: "status_delete",
actor: user,
subject_id: id
})
json(conn, %{})
end
end
def list_log(conn, params) do
{page, page_size} = page_params(params)
log =
ModerationLog.get_all(%{
page: page,
page_size: page_size,
start_date: params["start_date"],
end_date: params["end_date"],
user_id: params["user_id"],
search: params["search"]
})
conn
|> put_view(ModerationLogView)
|> render("index.json", %{log: log})
end
def config_descriptions(conn, _params) do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, @descriptions_json)
end
def config_show(conn, %{"only_db" => true}) do
with :ok <- configurable_from_database(conn) do
configs = Pleroma.Repo.all(ConfigDB)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
end
def config_show(conn, _params) do
with :ok <- configurable_from_database(conn) do
configs = ConfigDB.get_all_as_keyword()
merged =
Config.Holder.default_config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
db =
if configs[group][key] do
ConfigDB.get_db_keys(configs[group][key], key)
end
db_value = configs[group][key]
merged_value =
if !is_nil(db_value) and Keyword.keyword?(db_value) and
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
ConfigDB.merge_group(group, key, value, db_value)
else
value
end
setting = %{
group: ConfigDB.convert(group),
key: ConfigDB.convert(key),
value: ConfigDB.convert(merged_value)
}
if db, do: Map.put(setting, :db, db), else: setting
end)
end)
|> List.flatten()
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
def config_update(conn, %{"configs" => configs}) do
with :ok <- configurable_from_database(conn) do
{_errors, results} =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
%{"group" => group, "key" => key, "value" => value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.split_with(fn result -> elem(result, 0) == :error end)
{deleted, updated} =
results
|> Enum.map(fn {:ok, config} ->
Map.put(config, :db, ConfigDB.get_db_keys(config))
end)
|> Enum.split_with(fn config ->
Ecto.get_meta(config, :state) == :deleted
end)
Config.TransferTask.load_and_update_env(deleted, false)
if !Restarter.Pleroma.need_reboot?() do
changed_reboot_settings? =
(updated ++ deleted)
|> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
def restart(conn, _params) do
with :ok <- configurable_from_database(conn) do
Restarter.Pleroma.restart(Config.get(:env), 50)
json(conn, %{})
end
end
def need_reboot(conn, _params) do
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
end
defp configurable_from_database(conn) do
if Config.get(:configurable_from_database) do
:ok
else
errors(
conn,
{:error, "To use this endpoint you need to enable configuration from database."}
)
end
end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
conn |> json("ok")
end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.toggle_confirmation(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "confirm_email"
})
conn |> json("")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.try_send_confirmation_email(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "resend_confirmation_email"
})
conn |> json("")
end
def oauth_app_create(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
result =
case App.create(params) do
{:ok, app} ->
AppView.render("show.json", %{app: app, admin: true})
{:error, changeset} ->
App.errors(changeset)
end
json(conn, result)
end
def oauth_app_update(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
with {:ok, app} <- App.update(params) do
json(conn, AppView.render("show.json", %{app: app, admin: true}))
else
{:error, changeset} ->
json(conn, App.errors(changeset))
nil ->
json_response(conn, :bad_request, "")
end
end
def oauth_app_list(conn, params) do
{page, page_size} = page_params(params)
search_params = %{
client_name: params["name"],
client_id: params["client_id"],
page: page,
page_size: page_size
}
search_params =
if Map.has_key?(params, "trusted") do
Map.put(search_params, :trusted, params["trusted"])
else
search_params
end
with {:ok, apps, count} <- App.search(search_params) do
json(
conn,
AppView.render("index.json",
apps: apps,
count: count,
page_size: page_size,
admin: true
)
)
end
end
def oauth_app_delete(conn, params) do
with {:ok, _app} <- App.destroy(params["id"]) do
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def stats(conn, _) do
count = Stats.get_status_visibility_count()
conn
|> json(%{"status_visibility" => count})
end
defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
defp errors(conn, {:error, reason}) do
conn
|> put_status(:bad_request)
|> json(reason)
end
defp errors(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
defp errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])}
end
defp get_page(page_string) when is_nil(page_string), do: 1
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end
end
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
new file mode 100644
index 000000000..bd9026237
--- /dev/null
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -0,0 +1,139 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.CastAndValidate do
+ @moduledoc """
+ This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
+ (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
+ The main difference is ignoring unexpected query params instead of throwing
+ an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
+ to disable this behavior. Also, the default rendering error module
+ is `Pleroma.Web.ApiSpec.RenderError`.
+ """
+
+ @behaviour Plug
+
+ alias Plug.Conn
+
+ @impl Plug
+ def init(opts) do
+ opts
+ |> Map.new()
+ |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
+ end
+
+ @impl Plug
+ def call(%{private: %{open_api_spex: private_data}} = conn, %{
+ operation_id: operation_id,
+ render_error: render_error
+ }) do
+ spec = private_data.spec
+ operation = private_data.operation_lookup[operation_id]
+
+ content_type =
+ case Conn.get_req_header(conn, "content-type") do
+ [header_value | _] ->
+ header_value
+ |> String.split(";")
+ |> List.first()
+
+ _ ->
+ nil
+ end
+
+ private_data = Map.put(private_data, :operation_id, operation_id)
+ conn = Conn.put_private(conn, :open_api_spex, private_data)
+
+ case cast_and_validate(spec, operation, conn, content_type, strict?()) do
+ {:ok, conn} ->
+ conn
+
+ {:error, reason} ->
+ opts = render_error.init(reason)
+
+ conn
+ |> render_error.call(opts)
+ |> Plug.Conn.halt()
+ end
+ end
+
+ def call(
+ %{
+ private: %{
+ phoenix_controller: controller,
+ phoenix_action: action,
+ open_api_spex: private_data
+ }
+ } = conn,
+ opts
+ ) do
+ operation =
+ case private_data.operation_lookup[{controller, action}] do
+ nil ->
+ operation_id = controller.open_api_operation(action).operationId
+ operation = private_data.operation_lookup[operation_id]
+
+ operation_lookup =
+ private_data.operation_lookup
+ |> Map.put({controller, action}, operation)
+
+ OpenApiSpex.Plug.Cache.adapter().put(
+ private_data.spec_module,
+ {private_data.spec, operation_lookup}
+ )
+
+ operation
+
+ operation ->
+ operation
+ end
+
+ if operation.operationId do
+ call(conn, Map.put(opts, :operation_id, operation.operationId))
+ else
+ raise "operationId was not found in action API spec"
+ end
+ end
+
+ def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
+
+ defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+
+ defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
+ case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
+ {:ok, conn} ->
+ {:ok, conn}
+
+ # Remove unexpected query params and cast/validate again
+ {:error, errors} ->
+ query_params =
+ Enum.reduce(errors, conn.query_params, fn
+ %{reason: :unexpected_field, name: name, path: [name]}, params ->
+ Map.delete(params, name)
+
+ %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
+ path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
+ update_in(params, path, &List.delete(&1, value))
+
+ _, params ->
+ params
+ end)
+
+ conn = %Conn{conn | query_params: query_params}
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+ end
+
+ defp list_items_to_string(list) do
+ Enum.map(list, fn
+ i when is_atom(i) -> to_string(i)
+ i -> i
+ end)
+ end
+
+ defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
+end
diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
new file mode 100644
index 000000000..fe675a923
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -0,0 +1,96 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "View scheduled statuses",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: pagination_params(),
+ operationId: "ScheduledActivity.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of ScheduledStatus", "application/json", %Schema{
+ type: :array,
+ items: ScheduledStatus
+ })
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "View a single scheduled status",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ operationId: "ScheduledActivity.show",
+ responses: %{
+ 200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "Schedule a status",
+ operationId: "ScheduledActivity.update",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ scheduled_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ description:
+ "ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
+ }
+ }
+ }),
+ responses: %{
+ 200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "Cancel a scheduled status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ parameters: [id_param()],
+ operationId: "ScheduledActivity.delete",
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ example: "123",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
index b5877ca9c..d476b8ef3 100644
--- a/lib/pleroma/web/api_spec/render_error.ex
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -1,231 +1,234 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.RenderError do
@behaviour Plug
import Plug.Conn, only: [put_status: 2]
import Phoenix.Controller, only: [json: 2]
import Pleroma.Web.Gettext
@impl Plug
def init(opts), do: opts
@impl Plug
def call(conn, errors) do
errors =
Enum.map(errors, fn
+ %{name: nil, reason: :invalid_enum} = err ->
+ %OpenApiSpex.Cast.Error{err | name: err.value}
+
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
err ->
err
end)
conn
|> put_status(:bad_request)
|> json(%{
error: errors |> Enum.map(&message/1) |> Enum.join(" "),
errors: errors |> Enum.map(&render_error/1)
})
end
defp render_error(error) do
pointer = OpenApiSpex.path_to_string(error)
%{
title: "Invalid value",
source: %{
pointer: pointer
},
message: OpenApiSpex.Cast.Error.message(error)
}
end
defp message(%{reason: :invalid_schema_type, type: type, name: name}) do
gettext("%{name} - Invalid schema.type. Got: %{type}.",
name: name,
type: inspect(type)
)
end
defp message(%{reason: :null_value, name: name} = error) do
case error.type do
nil ->
gettext("%{name} - null value.", name: name)
type ->
gettext("%{name} - null value where %{type} expected.",
name: name,
type: type
)
end
end
defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do
gettext(
"Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.",
invalid_schema: invalid_schema
)
end
defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do
gettext("Failed to cast value using any of: %{failed_schemas}.",
failed_schemas: failed_schemas
)
end
defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do
gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas)
end
defp message(%{reason: :min_length, length: length, name: name}) do
gettext("%{name} - String length is smaller than minLength: %{length}.",
name: name,
length: length
)
end
defp message(%{reason: :max_length, length: length, name: name}) do
gettext("%{name} - String length is larger than maxLength: %{length}.",
name: name,
length: length
)
end
defp message(%{reason: :unique_items, name: name}) do
gettext("%{name} - Array items must be unique.", name: name)
end
defp message(%{reason: :min_items, length: min, value: array, name: name}) do
gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.",
name: name,
length: length(array),
min: min
)
end
defp message(%{reason: :max_items, length: max, value: array, name: name}) do
gettext("%{name} - Array length %{length} is larger than maxItems: %{}.",
name: name,
length: length(array),
max: max
)
end
defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do
gettext("%{name} - %{count} is not a multiple of %{multiple}.",
name: name,
count: count,
multiple: multiple
)
end
defp message(%{reason: :exclusive_max, length: max, value: value, name: name})
when value >= max do
gettext("%{name} - %{value} is larger than exclusive maximum %{max}.",
name: name,
value: value,
max: max
)
end
defp message(%{reason: :maximum, length: max, value: value, name: name})
when value > max do
gettext("%{name} - %{value} is larger than inclusive maximum %{max}.",
name: name,
value: value,
max: max
)
end
defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name})
when value <= min do
gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.",
name: name,
value: value,
min: min
)
end
defp message(%{reason: :minimum, length: min, value: value, name: name})
when value < min do
gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.",
name: name,
value: value,
min: min
)
end
defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do
gettext("%{name} - Invalid %{type}. Got: %{value}.",
name: name,
value: OpenApiSpex.TermType.type(value),
type: type
)
end
defp message(%{reason: :invalid_format, format: format, name: name}) do
gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format))
end
defp message(%{reason: :invalid_enum, name: name}) do
gettext("%{name} - Invalid value for enum.", name: name)
end
defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
gettext("Failed to cast to any schema in %{polymorphic_type}",
polymorphic_type: polymorphic_type
)
end
defp message(%{reason: :unexpected_field, name: name}) do
gettext("Unexpected field: %{name}.", name: safe_string(name))
end
defp message(%{reason: :no_value_for_discriminator, name: field}) do
gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field)
end
defp message(%{reason: :invalid_discriminator_value, name: field}) do
gettext("No value provided for required discriminator `%{field}`.", name: field)
end
defp message(%{reason: :unknown_schema, name: name}) do
gettext("Unknown schema: %{name}.", name: name)
end
defp message(%{reason: :missing_field, name: name}) do
gettext("Missing field: %{name}.", name: name)
end
defp message(%{reason: :missing_header, name: name}) do
gettext("Missing header: %{name}.", name: name)
end
defp message(%{reason: :invalid_header, name: name}) do
gettext("Invalid value for header: %{name}.", name: name)
end
defp message(%{reason: :max_properties, meta: meta}) do
gettext(
"Object property count %{property_count} is greater than maxProperties: %{max_properties}.",
property_count: meta.property_count,
max_properties: meta.max_properties
)
end
defp message(%{reason: :min_properties, meta: meta}) do
gettext(
"Object property count %{property_count} is less than minProperties: %{min_properties}",
property_count: meta.property_count,
min_properties: meta.min_properties
)
end
defp safe_string(string) do
to_string(string) |> String.slice(0..39)
end
end
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
new file mode 100644
index 000000000..c146c416e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Attachment",
+ description: "Represents a file or media attachment that can be added to a status.",
+ type: :object,
+ requried: [:id, :url, :preview_url],
+ properties: %{
+ id: %Schema{type: :string},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of the original full-size attachment"
+ },
+ remote_url: %Schema{
+ type: :string,
+ format: :uri,
+ description:
+ "The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
+ nullable: true
+ },
+ preview_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of a scaled-down preview of the attachment"
+ },
+ text_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A shorter URL for the attachment"
+ },
+ description: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
+ },
+ type: %Schema{
+ type: :string,
+ enum: ["image", "video", "audio", "unknown"],
+ description: "The type of the attachment"
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ mime_type: %Schema{type: :string, description: "mime type of the attachment"}
+ }
+ }
+ },
+ example: %{
+ id: "1638338801",
+ type: "image",
+ url: "someurl",
+ remote_url: "someurl",
+ preview_url: "someurl",
+ text_url: "someurl",
+ description: nil,
+ pleroma: %{mime_type: "image/png"}
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
new file mode 100644
index 000000000..0520d0848
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ScheduledStatus",
+ description: "Represents a status that will be published at a future scheduled date.",
+ type: :object,
+ required: [:id, :scheduled_at, :params],
+ properties: %{
+ id: %Schema{type: :string},
+ scheduled_at: %Schema{type: :string, format: :"date-time"},
+ media_attachments: %Schema{type: :array, items: Attachment},
+ params: %Schema{
+ type: :object,
+ required: [:text, :visibility],
+ properties: %{
+ text: %Schema{type: :string, nullable: true},
+ media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
+ sensitive: %Schema{type: :boolean, nullable: true},
+ spoiler_text: %Schema{type: :string, nullable: true},
+ visibility: %Schema{type: VisibilityScope, nullable: true},
+ scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
+ poll: %Schema{type: Poll, nullable: true},
+ in_reply_to_id: %Schema{type: :string, nullable: true}
+ }
+ }
+ },
+ example: %{
+ id: "3221",
+ scheduled_at: "2019-12-05T12:33:01.000Z",
+ params: %{
+ text: "test content",
+ media_ids: nil,
+ sensitive: nil,
+ spoiler_text: nil,
+ visibility: nil,
+ scheduled_at: nil,
+ poll: nil,
+ idempotency: nil,
+ in_reply_to_id: nil
+ },
+ media_attachments: [Attachment.schema().example]
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index aef0588d4..d44636a48 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -1,226 +1,212 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Status",
description: "Response schema for a status",
type: :object,
properties: %{
account: Account,
application: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
website: %Schema{type: :string, nullable: true, format: :uri}
}
},
bookmarked: %Schema{type: :boolean},
card: %Schema{
type: :object,
nullable: true,
properties: %{
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
provider_name: %Schema{type: :string, nullable: true},
provider_url: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, format: :uri},
image: %Schema{type: :string, nullable: true, format: :uri},
title: %Schema{type: :string},
description: %Schema{type: :string}
}
},
content: %Schema{type: :string, format: :html},
created_at: %Schema{type: :string, format: "date-time"},
emojis: %Schema{type: :array, items: Emoji},
favourited: %Schema{type: :boolean},
favourites_count: %Schema{type: :integer},
id: FlakeID,
in_reply_to_account_id: %Schema{type: :string, nullable: true},
in_reply_to_id: %Schema{type: :string, nullable: true},
language: %Schema{type: :string, nullable: true},
media_attachments: %Schema{
type: :array,
- items: %Schema{
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri},
- remote_url: %Schema{type: :string, format: :uri},
- preview_url: %Schema{type: :string, format: :uri},
- text_url: %Schema{type: :string, format: :uri},
- description: %Schema{type: :string},
- type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
- pleroma: %Schema{
- type: :object,
- properties: %{mime_type: %Schema{type: :string}}
- }
- }
- }
+ items: Attachment
},
mentions: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
acct: %Schema{type: :string},
username: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
muted: %Schema{type: :boolean},
pinned: %Schema{type: :boolean},
pleroma: %Schema{
type: :object,
properties: %{
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
conversation_id: %Schema{type: :integer},
direct_conversation_id: %Schema{type: :string, nullable: true},
emoji_reactions: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
count: %Schema{type: :integer},
me: %Schema{type: :boolean}
}
}
},
expires_at: %Schema{type: :string, format: "date-time", nullable: true},
in_reply_to_account_acct: %Schema{type: :string, nullable: true},
local: %Schema{type: :boolean},
spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
thread_muted: %Schema{type: :boolean}
}
},
poll: %Schema{type: Poll, nullable: true},
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true
},
reblogged: %Schema{type: :boolean},
reblogs_count: %Schema{type: :integer},
replies_count: %Schema{type: :integer},
sensitive: %Schema{type: :boolean},
spoiler_text: %Schema{type: :string},
tags: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
uri: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, nullable: true, format: :uri},
visibility: VisibilityScope
},
example: %{
"account" => %{
"acct" => "nick6",
"avatar" => "http://localhost:4001/images/avi.png",
"avatar_static" => "http://localhost:4001/images/avi.png",
"bot" => false,
"created_at" => "2020-04-07T19:48:51.000Z",
"display_name" => "Test テスト User 6",
"emojis" => [],
"fields" => [],
"followers_count" => 1,
"following_count" => 0,
"header" => "http://localhost:4001/images/banner.png",
"header_static" => "http://localhost:4001/images/banner.png",
"id" => "9toJCsKN7SmSf3aj5c",
"locked" => false,
"note" => "Tester Number 6",
"pleroma" => %{
"background_image" => nil,
"confirmation_pending" => false,
"hide_favorites" => true,
"hide_followers" => false,
"hide_followers_count" => false,
"hide_follows" => false,
"hide_follows_count" => false,
"is_admin" => false,
"is_moderator" => false,
"relationship" => %{
"blocked_by" => false,
"blocking" => false,
"domain_blocking" => false,
"endorsed" => false,
"followed_by" => false,
"following" => true,
"id" => "9toJCsKN7SmSf3aj5c",
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"showing_reblogs" => true,
"subscribing" => false
},
"skip_thread_containment" => false,
"tags" => []
},
"source" => %{
"fields" => [],
"note" => "Tester Number 6",
"pleroma" => %{"actor_type" => "Person", "discoverable" => false},
"sensitive" => false
},
"statuses_count" => 1,
"url" => "http://localhost:4001/users/nick6",
"username" => "nick6"
},
"application" => %{"name" => "Web", "website" => nil},
"bookmarked" => false,
"card" => nil,
"content" => "foobar",
"created_at" => "2020-04-07T19:48:51.000Z",
"emojis" => [],
"favourited" => false,
"favourites_count" => 0,
"id" => "9toJCu5YZW7O7gfvH6",
"in_reply_to_account_id" => nil,
"in_reply_to_id" => nil,
"language" => nil,
"media_attachments" => [],
"mentions" => [],
"muted" => false,
"pinned" => false,
"pleroma" => %{
"content" => %{"text/plain" => "foobar"},
"conversation_id" => 345_972,
"direct_conversation_id" => nil,
"emoji_reactions" => [],
"expires_at" => nil,
"in_reply_to_account_acct" => nil,
"local" => true,
"spoiler_text" => %{"text/plain" => ""},
"thread_muted" => false
},
"poll" => nil,
"reblog" => nil,
"reblogged" => false,
"reblogs_count" => 0,
"replies_count" => 0,
"sensitive" => false,
"spoiler_text" => "",
"tags" => [],
"uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
"url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
"visibility" => "private"
}
})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 61b0e2f63..8458cbdd5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,397 +1,397 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [
add_link_headers: 2,
truthy_param?: 1,
assign_account_by_id: 2,
json_response: 3,
skip_relationships?: 1
]
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:show, :followers, :following]
)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :blocks
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(
RateLimiter,
[name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts"
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else
{:error, error} -> json_response(conn, :bad_request, %{error: error})
end
end
def create(%{assigns: %{app: _app}} = conn, _) do
render_error(conn, :bad_request, "Missing parameters")
end
def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end
@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json",
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
)
end
@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
user = original_user
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{})
user_params =
[
:no_rich_text,
:locked,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment,
:allow_following_move,
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
end)
|> add_if_present(params, :display_name, :name)
|> add_if_present(params, :note, :bio)
|> add_if_present(params, :avatar, :avatar)
|> add_if_present(params, :header, :banner)
|> add_if_present(params, :pleroma_background_image, :background)
|> add_if_present(
params,
:fields_attributes,
:raw_fields,
&{:ok, normalize_fields_attributes(&1)}
)
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|> add_if_present(params, :default_scope, :default_scope)
|> add_if_present(params, :actor_type, :actor_type)
changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
_ -> map
end
end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
Enum.map(fields, fn
%{} = field -> %{"name" => field.name, "value" => field.value}
field -> field
end)
end
end
@doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
_e -> render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
true <- User.visible_for?(user, reading_user) do
params =
params
|> Map.delete(:tagged)
|> Enum.filter(&(not is_nil(&1)))
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("tag", params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json",
activities: activities,
for: reading_user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else
_e -> render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
user.hide_followers -> []
true -> MastodonAPI.get_followers(user, params)
end
conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
user.hide_follows -> []
true -> MastodonAPI.get_friends(user, params)
end
conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end
@doc "GET /api/v1/accounts/:id/lists"
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
lists = Pleroma.List.get_lists_account_belongs(user, account)
conn
|> put_view(ListView)
|> render("index.json", lists: lists)
end
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"}
end
def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
render(conn, "relationship.json", user: follower, target: followed)
end
end
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, _user_relationships} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _user_block} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _user_block} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn
|> assign(:account, user)
|> follow(%{})
nil ->
{:error, :not_found}
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
users = User.muted_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
users = User.blocked_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 408e11474..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -1,53 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(
:skip_plug,
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
when action == :create
)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
@doc "POST /api/v1/apps"
def create(%{body_params: params} = conn, _params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
|> Map.take([:client_name, :redirect_uris, :website])
|> 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
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 000ad743f..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -1,21 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
:skip_plug,
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
when action == :index
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index c4fa383f2..825b231ab 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -1,40 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action != :index
)
@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: user}} = conn, _) do
json(conn, Map.get(user, :domain_blocks, []))
end
@doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index a14c86893..596b85617 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -1,99 +1,99 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.MastodonAPI.MastodonAPI
@oauth_read_actions [:show, :index]
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
)
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
# GET /api/v1/notifications
def index(conn, %{account_id: account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
params
|> Map.delete(:account_id)
|> Map.put(:account_ap_id, account_ap_id)
index(conn, params)
_ ->
conn
|> put_status(:not_found)
|> json(%{"error" => "Account is not found"})
end
end
def index(%{assigns: %{user: user}} = conn, params) do
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(notifications)
|> render("index.json",
notifications: notifications,
for: user,
skip_relationships: skip_relationships?(params)
)
end
# GET /api/v1/notifications/:id
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
# POST /api/v1/notifications/clear
def clear(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
# POST /api/v1/notifications/:id/dismiss
def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss_via_body(%{body_params: params} = conn, _) do
dismiss(conn, params)
end
# DELETE /api/v1/notifications/destroy_multiple
def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f65c5c62b..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -1,23 +1,23 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportController do
alias Pleroma.Plugs.OAuthScopesPlug
use Pleroma.Web, :controller
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
@doc "POST /api/v1/reports"
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity)
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 899b78873..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -1,57 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
- plug(:assign_scheduled_activity when action != :index)
-
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
+ plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
+
@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
|> render("index.json", scheduled_activities: scheduled_activities)
end
end
@doc "GET /api/v1/scheduled_statuses/:id"
def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end
@doc "DELETE /api/v1/scheduled_statuses/:id"
def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5b00243e9..ef2239d59 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,703 +1,704 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
end
pipeline :expect_authentication do
plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
end
pipeline :expect_public_instance_or_authentication do
plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :api do
plug(:expect_public_instance_or_authentication)
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :authenticated_api do
plug(:expect_authentication)
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :admin_api do
plug(:expect_authentication)
plug(:base_api)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :mastodon_html do
plug(:browser)
plug(:authenticate)
plug(:after_auth)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
end
scope "/api/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
patch("/users/activate", AdminAPIController, :user_activate)
patch("/users/deactivate", AdminAPIController, :user_deactivate)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
get("/relay", AdminAPIController, :relay_list)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
post("/users/invite_token", AdminAPIController, :create_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", AdminAPIController, :list_reports)
get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
+ get("/statuses/:id", AdminAPIController, :status_show)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/statuses", AdminAPIController, :list_statuses)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
get("/oauth_app", AdminAPIController, :oauth_app_list)
post("/oauth_app", AdminAPIController, :oauth_app_create)
patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
# Modifying packs
scope "/packs" do
pipe_through(:admin_api)
get("/import", EmojiAPIController, :import_from_filesystem)
get("/remote", EmojiAPIController, :remote)
post("/download", EmojiAPIController, :download)
post("/:name", EmojiAPIController, :create)
patch("/:name", EmojiAPIController, :update)
delete("/:name", EmojiAPIController, :delete)
post("/:name/files", EmojiAPIController, :add_file)
patch("/:name/files", EmojiAPIController, :update_file)
delete("/:name/files", EmojiAPIController, :delete_file)
end
# Pack info / downloading
scope "/packs" do
get("/", EmojiAPIController, :list)
get("/:name", EmojiAPIController, :show)
get("/:name/archive", EmojiAPIController, :archive)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
end
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
end
scope [] do
pipe_through(:authenticated_api)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :new_scrobble)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
post("/follows", AccountController, :follow_by_uri)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
get("/apps/verify_credentials", AppController, :verify_credentials)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
# Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
end
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/search", SearchController, :search)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
post("/apps", AppController, :create)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
end
scope "/api" do
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
post(
"/qvitter/statuses/notifications/read",
TwitterAPI.Controller,
:mark_notifications_as_read
)
end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Plugs.StaticFEPlug)
end
pipeline :oembed do
plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Web do
pipe_through([:ostatus, :http_signature])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/web/manifest.json", MastoFEController, :manifest)
end
scope "/", Pleroma.Web do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/web/*path", MastoFEController, :index)
end
scope "/proxy/", Pleroma.Web.MediaProxy do
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
:do_oauth_check,
:fallback_oauth_check,
:skip_oauth_check,
:fallback_oauth_skip_publicity_check,
:skip_oauth_skip_publicity_check,
:missing_oauth_check_definition
]
scope "/test/api", Pleroma.Tests do
pipe_through(:api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/api*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector)
options("/*path", RedirectController, :empty)
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 7ffd0e51b..84ece1be2 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,228 +1,195 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
alias Pleroma.HTTP
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
require Logger
def host_meta do
base_url = Web.base_url()
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
{
:Link,
%{
rel: "lrdd",
type: "application/xrd+xml",
template: "#{base_url}/.well-known/webfinger?resource={uri}"
}
}
}
|> XmlBuilder.to_doc()
end
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_cached_by_nickname(username) do
{:ok, represent_user(user, fmt)}
else
_e ->
with %User{} = user <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, fmt)}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
defp gather_links(%User{} = user) do
[
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
}
] ++ Publisher.gather_webfinger_links(user)
end
def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user)
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id],
"links" => gather_links(user)
}
end
def represent_user(user, "XML") do
{:ok, user} = User.ensure_keys_present(user)
links =
gather_links(user)
|> Enum.map(fn link -> {:Link, link} end)
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
{:Alias, user.ap_id}
] ++ links
}
|> XmlBuilder.to_doc()
end
- defp get_magic_key("data:application/magic-public-key," <> magic_key) do
- {:ok, magic_key}
- end
-
- defp get_magic_key(nil) do
- Logger.debug("Undefined magic key.")
- {:ok, nil}
- end
+ defp webfinger_from_xml(doc) do
+ subject = XML.string_from_xpath("//Subject", doc)
- defp get_magic_key(_) do
- {:error, "Missing magic key data."}
- end
+ subscribe_address =
+ ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+ |> XML.string_from_xpath(doc)
- defp webfinger_from_xml(doc) do
- with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
- {:ok, magic_key} <- get_magic_key(magic_key),
- topic <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
- doc
- ),
- subject <- XML.string_from_xpath("//Subject", doc),
- subscribe_address <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
- doc
- ),
- ap_id <-
- XML.string_from_xpath(
- ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
- doc
- ) do
- data = %{
- "magic_key" => magic_key,
- "topic" => topic,
- "subject" => subject,
- "subscribe_address" => subscribe_address,
- "ap_id" => ap_id
- }
+ ap_id =
+ ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+ |> XML.string_from_xpath(doc)
- {:ok, data}
- else
- {:error, e} ->
- {:error, e}
+ data = %{
+ "subject" => subject,
+ "subscribe_address" => subscribe_address,
+ "ap_id" => ap_id
+ }
- e ->
- {:error, e}
- end
+ {:ok, data}
end
defp webfinger_from_json(doc) do
data =
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "ap_id", link["href"])
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
- {_, "http://ostatus.org/schema/1.0/subscribe"} ->
- Map.put(data, "subscribe_address", link["template"])
-
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
end
end)
{:ok, data}
end
def get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd']/@template"
with doc when doc != :error <- XML.parse_document(body),
template when template != nil <- XML.string_from_xpath(xpath, doc) do
{:ok, template}
end
end
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
HTTP.get("http://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body, status: status}} when status in 200..299 <-
HTTP.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
end
end
end
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
def finger(account) do
account = String.trim_leading(account, "@")
domain =
with [_name, domain] <- String.split(account, "@") do
domain
else
_e ->
URI.parse(account).host
end
address =
case find_lrdd_template(domain) do
{:ok, template} ->
String.replace(template, "{uri}", URI.encode(account))
_ ->
"https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
end
with response <-
HTTP.get(
address,
[{"accept", "application/xrd+xml,application/jrd+json"}]
),
{:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
if doc != :error do
webfinger_from_xml(doc)
else
with {:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
end
end
else
e ->
Logger.debug(fn -> "Couldn't finger #{account}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end
end
end
diff --git a/priv/repo/migrations/20200505072231_remove_magic_key_field.exs b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
new file mode 100644
index 000000000..2635e671b
--- /dev/null
+++ b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveMagicKeyField do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ remove(:magic_key, :string)
+ end
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index f0b797fd4..495764782 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,424 +1,425 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
def participation_factory do
conversation = insert(:conversation)
user = insert(:user)
%Pleroma.Conversation.Participation{
conversation: conversation,
user: user,
read: false
}
end
def conversation_factory do
%Pleroma.Conversation{
ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}")
}
end
def user_factory do
user = %User{
name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
last_digest_emailed_at: NaiveDateTime.utc_now(),
+ last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{}
}
%{
user
| ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
end
def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user)
relationship_type = attrs[:relationship_type] || :block
%Pleroma.UserRelationship{
source_id: source.id,
target_id: target.id,
relationship_type: relationship_type
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Note",
"content" => text,
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"likes" => [],
"like_count" => 0,
"context" => "2hu",
"summary" => "2hu",
"tag" => ["2hu"],
"emoji" => %{
"2hu" => "corndog.png"
}
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def audio_factory(attrs \\ %{}) do
text = sequence(:text, &"lain radio episode #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Audio",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"artist" => "lain",
"title" => text,
"album" => "lain radio",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"actor" => user.ap_id,
"length" => 180_000
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def listen_factory do
audio = insert(:audio)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Listen",
"actor" => audio.data["actor"],
"to" => audio.data["to"],
"object" => audio.data,
"published" => audio.data["published"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def direct_note_factory do
user2 = insert(:user)
%Pleroma.Object{data: data} = note_factory()
%Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})}
end
def article_factory do
note_factory()
|> Map.put("type", "Article")
end
def tombstone_factory do
data = %{
"type" => "Tombstone",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"formerType" => "Note",
"deleted" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Object{
data: data
}
end
def direct_note_activity_factory do
dm = insert(:direct_note)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => dm.data["actor"],
"to" => dm.data["to"],
"object" => dm.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => dm.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data_attrs = attrs[:data_attrs] || %{}
attrs = Map.drop(attrs, [:user, :note, :data_attrs])
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => note.data["actor"],
"to" => note.data["to"],
"object" => note.data["id"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"]
}
|> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end
defp expiration_offset_by_minutes(attrs, minutes) do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
|> NaiveDateTime.truncate(:second)
%Pleroma.ActivityExpiration{}
|> Map.merge(attrs)
|> Map.put(:scheduled_at, scheduled_at)
end
def expiration_in_the_past_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, -60)
end
def expiration_in_the_future_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, 61)
end
def article_activity_factory do
article = insert(:article)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => article.data["actor"],
"to" => article.data["to"],
"object" => article.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => article.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def announce_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
user = attrs[:user] || insert(:user)
data = %{
"type" => "Announce",
"actor" => note_activity.actor,
"object" => note_activity.data["id"],
"to" => [user.follower_address, note_activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => note_activity.data["context"]
}
%Pleroma.Activity{
data: data,
actor: user.ap_id,
recipients: data["to"]
}
end
def like_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => user.ap_id,
"type" => "Like",
"object" => object.data["id"],
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{
data: data
}
end
def follow_activity_factory do
follower = insert(:user)
followed = insert(:user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => follower.ap_id,
"type" => "Follow",
"object" => followed.ap_id,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Activity{
data: data,
actor: follower.ap_id
}
end
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: sequence(:client_name, &"Some client #{&1}"),
redirect_uris: "https://example.com/callback",
scopes: ["read", "write", "follow", "push", "admin"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
}
end
def instance_factory do
%Pleroma.Instances.Instance{
host: "domain.com",
unreachable_since: nil
}
end
def oauth_token_factory(attrs \\ %{}) do
scopes = Map.get(attrs, :scopes, ["read"])
oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end)
user = Map.get_lazy(attrs, :user, fn -> build(:user) end)
valid_until =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
scopes: scopes,
user: user,
app: oauth_app,
valid_until: valid_until
}
end
def oauth_admin_token_factory(attrs \\ %{}) do
user = Map.get_lazy(attrs, :user, fn -> build(:user, is_admin: true) end)
scopes =
attrs
|> Map.get(:scopes, ["admin"])
|> Kernel.++(["admin"])
|> Enum.uniq()
attrs = Map.merge(attrs, %{user: user, scopes: scopes})
oauth_token_factory(attrs)
end
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
scopes: ["read", "write", "follow", "push"],
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
user: build(:user),
app: build(:oauth_app)
}
end
def push_subscription_factory do
%Pleroma.Web.Push.Subscription{
user: build(:user),
token: build(:oauth_token),
endpoint: "https://example.com/example/1234",
key_auth: "8eDyX_uCN0XRhSbY5hs7Hg==",
key_p256dh:
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=",
data: %{}
}
end
def notification_factory do
%Pleroma.Notification{
user: build(:user)
}
end
def scheduled_activity_factory do
%Pleroma.ScheduledActivity{
user: build(:user),
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
params: build(:note) |> Map.from_struct() |> Map.get(:data)
}
end
def registration_factory do
user = insert(:user)
%Pleroma.Registration{
user: user,
provider: "twitter",
uid: "171799000",
info: %{
"name" => "John Doe",
"email" => "john@doe.com",
"nickname" => "johndoe",
"description" => "My bio"
}
}
end
def config_factory do
%Pleroma.ConfigDB{
key:
sequence(:key, fn key ->
# Atom dynamic registration hack in tests
"some_key_#{key}"
|> String.to_atom()
|> inspect()
end),
group: ":pleroma",
value:
sequence(
:value,
fn key ->
:erlang.term_to_binary(%{another_key: "#{key}somevalue", another: "#{key}somevalue"})
end
)
}
end
def marker_factory do
%Pleroma.Marker{
user: build(:user),
timeline: "notifications",
lock_version: 0,
last_read_id: "1"
}
end
end
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index e68e9bfd2..26281b45e 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -1,98 +1,104 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.Helpers do
@moduledoc """
Helpers for use in tests.
"""
alias Pleroma.Config
defmacro clear_config(config_path) do
quote do
clear_config(unquote(config_path)) do
end
end
end
defmacro clear_config(config_path, do: yield) do
quote do
initial_setting = Config.get(unquote(config_path))
unquote(yield)
on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
:ok
end
end
defmacro clear_config(config_path, temp_setting) do
quote do
clear_config(unquote(config_path)) do
Config.put(unquote(config_path), unquote(temp_setting))
end
end
end
defmacro __using__(_opts) do
quote do
import Pleroma.Tests.Helpers,
only: [
clear_config: 1,
clear_config: 2
]
- def to_datetime(naive_datetime) do
+ def to_datetime(%NaiveDateTime{} = naive_datetime) do
naive_datetime
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.truncate(:second)
end
+ def to_datetime(datetime) when is_binary(datetime) do
+ datetime
+ |> NaiveDateTime.from_iso8601!()
+ |> to_datetime()
+ end
+
def collect_ids(collection) do
collection
|> Enum.map(& &1.id)
|> Enum.sort()
end
def refresh_record(%{id: id, __struct__: model} = _),
do: refresh_record(model, %{id: id})
def refresh_record(model, %{id: id} = _) do
Pleroma.Repo.get_by(model, id: id)
end
# Used for comparing json rendering during tests.
def render_json(view, template, assigns) do
assigns = Map.new(assigns)
view.render(template, assigns)
|> Poison.encode!()
|> Poison.decode!()
end
def stringify_keys(nil), do: nil
def stringify_keys(key) when key in [true, false], do: key
def stringify_keys(key) when is_atom(key), do: Atom.to_string(key)
def stringify_keys(map) when is_map(map) do
map
|> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end)
|> Enum.into(%{})
end
def stringify_keys([head | rest] = list) when is_list(list) do
[stringify_keys(head) | stringify_keys(rest)]
end
def stringify_keys(key), do: key
defmacro guards_config(config_path) do
quote do
initial_setting = Config.get(config_path)
Config.put(config_path, true)
on_exit(fn -> Config.put(config_path, initial_setting) end)
end
end
end
end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index edd7dfb22..84ead93bb 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,2406 +1,2454 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
+ import ExUnit.CaptureLog
+ import Mock
import Pleroma.Factory
import Tesla.Mock
- import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :federating])
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
participations =
conversation.participations
|> Repo.preload(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
assert called(Pleroma.Web.Streamer.stream("participation", participations))
end
end
test "streams them out on activity creation" do
user_one = insert(:user)
user_two = insert(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
{:ok, activity} =
CommonAPI.post(user_one, %{
"status" => "@#{user_two.nickname}",
"visibility" => "direct"
})
conversation =
activity.data["context"]
|> Pleroma.Conversation.get_for_ap_id()
|> Repo.preload(participations: :user)
assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
end
end
end
describe "fetching restricted by visibility" do
test "it restricts by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
assert activities == [direct_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
assert activities == [unlisted_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
assert activities == [private_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
assert activities == [public_activity]
activities =
ActivityPub.fetch_activities([], %{
:visibility => ~w[private public],
"actor_id" => user.ap_id
})
assert activities == [public_activity, private_activity]
end
end
describe "fetching excluded by visibility" do
test "it excludes by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "direct",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
refute direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "unlisted",
"actor_id" => user.ap_id
})
assert public_activity in activities
refute unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "private",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
refute private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "public",
"actor_id" => user.ap_id
})
refute public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
assert user.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
test "it returns a user that is invisible" do
user_id = "http://mastodon.example.org/users/relay"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert User.invisible?(user)
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
fetch_two =
ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
|> Base.encode64()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "doesn't drop activities with content being null" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity.id == new_activity.id
end
test "inserts a given map into the activity database, giving it an id if it has none." do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert is_binary(activity.data["id"])
given_id = "bla"
data = %{
"id" => given_id,
"actor" => user.ap_id,
"to" => [],
"context" => "blabla",
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert activity.data["id"] == given_id
assert activity.data["context"] == "blabla"
assert activity.data["context_id"]
end
test "adds a context when none is there" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Pleroma.Object.normalize(activity)
assert is_binary(activity.data["context"])
assert is_binary(object.data["context"])
assert activity.data["context_id"]
assert object.data["context_id"]
end
test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert object = Object.normalize(activity)
assert is_binary(object.data["id"])
end
end
describe "listen activities" do
test "does not increase user note count" do
user = insert(:user)
{:ok, activity} =
ActivityPub.listen(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: user,
context: "",
object: %{
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"artist" => "lain",
"title" => "lain radio episode 1",
"length" => 180_000,
"type" => "Audio"
}
})
assert activity.actor == user.ap_id
user = User.get_cached_by_id(user.id)
assert user.note_count == 0
end
test "can be fetched into a timeline" do
_listen_activity_1 = insert(:listen)
_listen_activity_2 = insert(:listen)
_listen_activity_3 = insert(:listen)
timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
assert length(timeline) == 3
end
end
describe "create activities" do
test "it reverts create" do
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} =
ActivityPub.create(%{
to: ["user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "removes doubled 'to' recipients" do
user = insert(:user)
{:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == user.ap_id
assert activity.recipients == ["user1", "user2", user.ap_id]
end
test "increases user note count only for public activities" do
user = insert(:user)
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "1",
"visibility" => "public"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "unlisted"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "private"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "3",
"visibility" => "direct"
})
user = User.get_cached_by_id(user.id)
assert user.note_count == 2
end
test "increases replies count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
ap_id = activity.data["id"]
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
# unlisted
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# private
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# direct
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
end
end
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
{:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
{:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
activities = ActivityPub.fetch_activities(["someone", "someone_else"])
assert length(activities) == 2
assert activities == [activity_one, activity_two]
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
end
end
test "doesn't return blocked activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
assert Enum.member?(activities, activity_one)
refute Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_four)
end
test "doesn't return announce activities concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "does return activities from followed users on blocked domains" do
domain = "meanies.social"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
blocker = insert(:user)
{:ok, blocker} = User.follow(blocker, domain_user)
{:ok, blocker} = User.block_domain(blocker, domain)
assert User.following?(blocker, domain_user)
assert User.blocks_domain?(blocker, domain_user)
refute User.blocks?(blocker, domain_user)
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
assert activity in activities
# And check that if the guy we DO follow boosts someone else from their domain,
# that should be hidden
another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
bad_activity = insert(:note_activity, %{note: bad_note})
{:ok, repeat_activity, _} = CommonAPI.repeat(bad_activity.id, domain_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
activity_one_actor = User.get_by_ap_id(activity_one.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
# Calling with 'with_muted' will deliver muted activities, too.
activities =
ActivityPub.fetch_activities([], %{
"muting_user" => user,
"with_muted" => true,
"skip_preload" => true
})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, _user_mute} = User.unmute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_three_actor)
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return thread muted activities" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
end
test "returns thread muted activities when with_muted is set" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_two, _activity_one] =
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
end
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.follow(user, booster)
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)])
assert announce_activity.id == announce.id
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
{:ok, _unlisted_activity} =
CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
{:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
[activity] = ActivityPub.fetch_public_activities()
assert activity == listed_activity
end
test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1
assert Enum.at(activities, 0) == public
end
test "retrieves a maximum of 20 activities" do
ActivityBuilder.insert_list(10)
expected_activities = ActivityBuilder.insert_list(20)
activities = ActivityPub.fetch_public_activities()
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 20
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
expected_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 10
end
test "retrieves ids up to max_id" do
ActivityBuilder.insert_list(10)
expected_activities = ActivityBuilder.insert_list(20)
%{id: max_id} =
10
|> ActivityBuilder.insert_list()
|> List.first()
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
assert length(activities) == 20
assert collect_ids(activities) == collect_ids(expected_activities)
end
test "paginates via offset/limit" do
_first_part_activities = ActivityBuilder.insert_list(10)
second_part_activities = ActivityBuilder.insert_list(10)
later_activities = ActivityBuilder.insert_list(10)
activities =
ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
assert length(activities) == 20
assert collect_ids(activities) ==
collect_ids(second_part_activities) ++ collect_ids(later_activities)
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
end
describe "react to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
end
test "adds an emoji reaction activity to the db" do
user = insert(:user)
reactor = insert(:user)
third_user = insert(:user)
fourth_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert reaction_activity
assert reaction_activity.data["actor"] == reactor.ap_id
assert reaction_activity.data["type"] == "EmojiReact"
assert reaction_activity.data["content"] == "🔥"
assert reaction_activity.data["object"] == object.data["id"]
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1
assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
assert object.data["reaction_count"] == 2
assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
assert object.data["reaction_count"] == 3
assert object.data["reactions"] == [
["🔥", [fourth_user.ap_id, reactor.ap_id]],
["☕", [third_user.ap_id]]
]
end
test "reverts emoji reaction on error" do
[user, reactor] = insert_list(2, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
object = Object.normalize(activity)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
end
object = Object.get_by_ap_id(object.data["id"])
refute object.data["reaction_count"]
refute object.data["reactions"]
end
end
describe "unreacting to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert called(Federator.publish(unreaction_activity))
end
test "adds an undo activity to the db" do
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert unreaction_activity.actor == reactor.ap_id
assert unreaction_activity.data["object"] == reaction_activity.data["id"]
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == []
end
test "reverts emoji unreact on error" do
[user, reactor] = insert_list(2, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
end
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 1
assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
end
end
describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish())
{:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert called(Federator.publish(unlike_activity))
end
test "reverts unliking on error" do
note_activity = insert(:note_activity)
user = insert(:user)
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.normalize(note_activity)
assert object.data["like_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unlike(user, object)
end
assert Object.get_by_ap_id(object.data["id"]) == object
assert object.data["like_count"] == 1
assert Activity.get_by_id(like_activity.id)
end
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
assert note_activity.actor in unlike_activity.recipients
end
end
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
test "reverts annouce from object on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.announce(user, object)
end
reloaded_object = Object.get_by_ap_id(object.data["id"])
assert reloaded_object == object
refute reloaded_object.data["announcement_count"]
refute reloaded_object.data["announcements"]
end
end
describe "announcing a private object" do
test "adds an announce activity to the db if the audience is not widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
{:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
assert announce_activity.data["to"] == [User.ap_followers(user)]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
test "does not add an announce activity to the db if the audience is widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
end
test "does not add an announce activity to the db if the announcer is not the author" do
user = insert(:user)
announcer = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
end
end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unannouncing an object that is not announced does nothing
{:ok, object} = ActivityPub.unannounce(user, object)
refute object.data["announcement_count"]
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
object.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
test "reverts unannouncing on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, _announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unannounce(user, object)
end
object = Object.get_by_ap_id(object.data["id"])
assert object.data["announcement_count"] == 1
end
end
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} = ActivityPub.upload(file)
assert object.data["name"] == "an_image.jpg"
end
test "works with base64 encoded images" do
file = %{
"img" => data_uri()
}
{:ok, %Object{}} = ActivityPub.upload(file)
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = Repo.get_by(User, ap_id: activity.data["actor"])
followed = Repo.get_by(User, ap_id: activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "following / unfollowing" do
test "it reverts follow activity" do
follower = insert(:user)
followed = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.follow(follower, followed)
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "it reverts unfollow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
end
activity = Activity.get_by_id(follow_activity.id)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.follow(follower, followed)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Follow"
assert embedded_object["object"] == followed.ap_id
assert embedded_object["id"] == follow_activity.data["id"]
end
test "creates an undo activity for a pending follow request" do
follower = insert(:user)
followed = insert(:user, %{locked: true})
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Follow"
assert embedded_object["object"] == followed.ap_id
assert embedded_object["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "reverts block activity on error" do
[blocker, blocked] = insert_list(2, :user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "reverts unblock activity on error" do
[blocker, blocked] = insert_list(2, :user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
end
assert block_activity.data["type"] == "Block"
assert block_activity.data["actor"] == blocker.ap_id
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.aggregate(Object, :count, :id) == 1
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end
describe "deletion" do
setup do: clear_config([:instance, :rewrite_policy])
test "it reverts deletion on error" do
note = insert(:note_activity)
object = Object.normalize(note)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.delete(object)
end
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
assert Activity.get_by_id(note.id) == note
end
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == object.data["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "it doesn't fail when an activity was already deleted" do
{:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
end
test "decrements user note count only for public activities" do
user = insert(:user, note_count: 10)
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
assert user.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, object} =
object
|> Object.change(%{
data: %{
"actor" => object.data["actor"],
"id" => object.data["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
test "it passes delete activity through MRF before deleting the object" do
Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
note = insert(:note_activity)
object = Object.normalize(note)
{:error, {:reject, _}} = ActivityPub.delete(object)
assert Activity.get_by_id(note.id)
assert Repo.get(Object, object.id).data["type"] == object.data["type"]
end
end
describe "timeline post-processing" do
test "it filters broken threads" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, user1} = User.follow(user1, user3)
assert User.following?(user1, user3)
{:ok, user2} = User.follow(user2, user3)
assert User.following?(user2, user3)
{:ok, user3} = User.follow(user3, user2)
assert User.following?(user3, user2)
{:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
{:ok, private_activity_1} =
CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
{:ok, private_activity_2} =
CommonAPI.post(user2, %{
"status" => "hi 3",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_1.id
})
{:ok, private_activity_3} =
CommonAPI.post(user3, %{
"status" => "hi 4",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_2.id
})
activities =
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
assert length(activities) == 3
activities =
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2
end
end
describe "update" do
setup do: clear_config([:instance, :max_pinned_statuses])
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address]
assert embedded_object = update.data["object"]
assert embedded_object["id"] == user_data["id"]
assert embedded_object["type"] == user_data["type"]
end
end
test "returned pinned statuses" do
Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_two.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_three.id, user)
user = refresh_record(user)
activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
assert 3 = length(activities)
end
describe "flag/1" do
setup do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, activity} = CommonAPI.post(target_account, %{"status" => content})
context = Utils.generate_context_id()
reporter_ap_id = reporter.ap_id
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id)
{:ok,
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
}}
end
test "it can create a Flag activity",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
} do
assert {:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => content,
"published" => activity_with_object.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_account})
}
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj]
}
} = activity
end
test_with_mock "strips status data from Flag, before federating it",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content
},
Utils,
[:passthrough],
[] do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
new_data =
put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]])
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
end
test "fetch_activities/2 returns activities addressed to a list " do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.follow(list, member)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
activity = Repo.preload(activity, :bookmark)
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
end
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
describe "fetch_activities_bounded" do
test "fetches private posts for followed users" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "thought I looked cute might delete later :3",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
assert result.id == activity.id
end
test "fetches only public posts for other users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe", "visibility" => "public"})
{:ok, _private_activity} =
CommonAPI.post(user, %{
"status" => "why is tenshi eating a corndog so cute?",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
assert result.id == activity.id
end
end
describe "fetch_follow_information_for_user" do
test "syncronizes following/followers counters" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.follower_count == 527
assert info.following_count == 267
end
test "detects hidden followers" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.hide_follows == false
end
test "detects hidden follows" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == false
assert follow_info.hide_follows == true
end
test "detects hidden follows/followers for friendica" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:8080/followers/fuser3",
following_address: "http://localhost:8080/following/fuser3"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.follower_count == 296
assert follow_info.following_count == 32
assert follow_info.hide_follows == true
end
test "doesn't crash when follower and following counters are hidden" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_hidden_counters/following" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
})
"http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
"http://localhost:4001/users/masto_hidden_counters/followers" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
})
"http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
following_address: "http://localhost:4001/users/masto_hidden_counters/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.follower_count == 0
assert follow_info.hide_follows == true
assert follow_info.following_count == 0
end
end
describe "fetch_favourites/3" do
test "returns a favourite activities sorted by adds to favorite" do
user = insert(:user)
other_user = insert(:user)
user1 = insert(:user)
user2 = insert(:user)
{:ok, a1} = CommonAPI.post(user1, %{"status" => "bla"})
{:ok, _a2} = CommonAPI.post(user2, %{"status" => "traps are happy"})
{:ok, a3} = CommonAPI.post(user2, %{"status" => "Trees Are "})
{:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
{:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
{:ok, _} = CommonAPI.favorite(user, a4.id)
{:ok, _} = CommonAPI.favorite(other_user, a3.id)
{:ok, _} = CommonAPI.favorite(user, a3.id)
{:ok, _} = CommonAPI.favorite(other_user, a5.id)
{:ok, _} = CommonAPI.favorite(user, a5.id)
{:ok, _} = CommonAPI.favorite(other_user, a4.id)
{:ok, _} = CommonAPI.favorite(user, a1.id)
{:ok, _} = CommonAPI.favorite(other_user, a1.id)
result = ActivityPub.fetch_favourites(user)
assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
result = ActivityPub.fetch_favourites(user, %{"limit" => 2})
assert Enum.map(result, & &1.id) == [a1.id, a5.id]
end
end
describe "Move activity" do
test "create" do
%{ap_id: old_ap_id} = old_user = insert(:user)
%{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
follower = insert(:user)
follower_move_opted_out = insert(:user, allow_following_move: false)
User.follow(follower, old_user)
User.follow(follower_move_opted_out, old_user)
assert User.following?(follower, old_user)
assert User.following?(follower_move_opted_out, old_user)
assert {:ok, activity} = ActivityPub.move(old_user, new_user)
assert %Activity{
actor: ^old_ap_id,
data: %{
"actor" => ^old_ap_id,
"object" => ^old_ap_id,
"target" => ^new_ap_id,
"type" => "Move"
},
local: true
} = activity
params = %{
"op" => "move_following",
"origin_id" => old_user.id,
"target_id" => new_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(params, nil)
refute User.following?(follower, old_user)
assert User.following?(follower, new_user)
assert User.following?(follower_move_opted_out, old_user)
refute User.following?(follower_move_opted_out, new_user)
activity = %Activity{activity | object: nil}
assert [%Notification{activity: ^activity}] = Notification.for_user(follower)
assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out)
end
test "old user must be in the new user's `also_known_as` list" do
old_user = insert(:user)
new_user = insert(:user)
assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
ActivityPub.move(old_user, new_user)
end
end
test "doesn't retrieve replies activities with exclude_replies" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
{:ok, _reply} =
CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})
[result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
assert result.id == activity.id
assert length(ActivityPub.fetch_public_activities()) == 2
end
describe "replies filtering with public messages" do
setup :public_messages
test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 16
end
test "public timeline with reply_visibility `following`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 14
visible_ids =
Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "public timeline with reply_visibility `self`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 10
visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 13
visible_ids =
Map.values(u1) ++
Map.values(u3) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u4[:r1],
u4[:r2]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline with reply_visibility `following`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 11
visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u3[:r1],
u4[:r1],
u4[:r2]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline with reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 9
visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u3[:r1],
u4[:r1]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end
describe "replies filtering with private messages" do
setup :private_messages
test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "home timeline", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 12
end
test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 12
end
test "home timeline with default reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 10
visible_ids =
Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end
defp public_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)
{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)
{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})
{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})
{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, r2_3} =
CommonAPI.post(u4, %{
"status" => "@#{u2.nickname} reply from u4 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})
{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, r3_3} =
CommonAPI.post(u4, %{
"status" => "@#{u3.nickname} reply from u4 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})
{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok, r4_2} =
CommonAPI.post(u2, %{
"status" => "@#{u4.nickname} reply from u2 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok, r4_3} =
CommonAPI.post(u3, %{
"status" => "@#{u4.nickname} reply from u3 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
end
defp private_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u3)
{:ok, u3} = User.follow(u3, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)
{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)
{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})
{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})
{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})
{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})
{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})
{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})
{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})
{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})
{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id,
"visibility" => "private"
})
{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id},
u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}}
end
+
+ describe "maybe_update_follow_information/1" do
+ setup do
+ clear_config([:instance, :external_user_synchronization], true)
+
+ user = %{
+ local: false,
+ ap_id: "https://gensokyo.2hu/users/raymoo",
+ following_address: "https://gensokyo.2hu/users/following",
+ follower_address: "https://gensokyo.2hu/users/followers",
+ type: "Person"
+ }
+
+ %{user: user}
+ end
+
+ test "logs an error when it can't fetch the info", %{user: user} do
+ assert capture_log(fn ->
+ ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "just returns the input if the user type is Application", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:type, "Application")
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "it just returns the input if the user has no following/follower addresses", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:following_address, nil)
+ |> Map.put(:follower_address, nil)
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+ end
end
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
index 3c5c3696e..93989e28a 100644
--- a/test/web/activity_pub/object_validator_test.exs
+++ b/test/web/activity_pub/object_validator_test.exs
@@ -1,83 +1,109 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
describe "likes" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
valid_like = %{
"to" => [user.ap_id],
"cc" => [],
"type" => "Like",
"id" => Utils.generate_activity_id(),
"object" => post_activity.data["object"],
"actor" => user.ap_id,
"context" => "a context"
}
%{valid_like: valid_like, user: user, post_activity: post_activity}
end
test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
{:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
assert "id" in Map.keys(object)
end
test "is valid for a valid object", %{valid_like: valid_like} do
assert LikeValidator.cast_and_validate(valid_like).valid?
end
+ test "sets the 'to' field to the object actor if no recipients are given", %{
+ valid_like: valid_like,
+ user: user
+ } do
+ without_recipients =
+ valid_like
+ |> Map.delete("to")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_recipients, [])
+
+ assert object["to"] == [user.ap_id]
+ end
+
+ test "sets the context field to the context of the object if no context is given", %{
+ valid_like: valid_like,
+ post_activity: post_activity
+ } do
+ without_context =
+ valid_like
+ |> Map.delete("context")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_context, [])
+
+ assert object["context"] == post_activity.data["context"]
+ end
+
test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
without_actor = Map.delete(valid_like, "actor")
refute LikeValidator.cast_and_validate(without_actor).valid?
with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
end
test "it errors when the object is missing or not known", %{valid_like: valid_like} do
without_object = Map.delete(valid_like, "object")
refute LikeValidator.cast_and_validate(without_object).valid?
with_invalid_object = Map.put(valid_like, "object", "invalidobject")
refute LikeValidator.cast_and_validate(with_invalid_object).valid?
end
test "it errors when the actor has already like the object", %{
valid_like: valid_like,
user: user,
post_activity: post_activity
} do
_like = CommonAPI.favorite(user, post_activity.id)
refute LikeValidator.cast_and_validate(valid_like).valid?
end
test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
wrapped_like =
valid_like
|> Map.put("actor", %{"id" => valid_like["actor"]})
|> Map.put("object", %{"id" => valid_like["object"]})
validated = LikeValidator.cast_and_validate(wrapped_like)
assert validated.valid?
assert {:actor, valid_like["actor"]} in validated.changes
assert {:object, valid_like["object"]} in validated.changes
end
end
end
diff --git a/test/web/activity_pub/transmogrifier/like_handling_test.exs b/test/web/activity_pub/transmogrifier/like_handling_test.exs
new file mode 100644
index 000000000..54a5c1dbc
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/like_handling_test.exs
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "it works for incoming likes" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/mastodon-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ refute Enum.empty?(activity.recipients)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Like"
+ assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
+ assert data["object"] == activity.data["object"]
+ end
+
+ test "it works for incoming misskey likes, turning them into EmojiReacts" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == "🍮"
+ end
+
+ test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("_misskey_reaction", "⭐")
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == "⭐"
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 36e1e7bd1..23efa4be6 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,2209 +1,2153 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Mock
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("object", Object.normalize(activity).data)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity == returned_activity
end
@tag capture_log: true
test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = Map.put(data, "object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
assert activity =
Activity.get_create_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
end
test "it does not fetch reply-to activities beyond max replies depth limit" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = Map.put(data, "object", object)
with_mock Pleroma.Web.Federator,
allowed_thread_distance?: fn _ -> false end do
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
refute Activity.get_create_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_object.data["inReplyToAtomUri"] ==
"https://shitposter.club/notice/2827873"
end
end
test "it does not crash if the object in inReplyTo can't be fetched" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://404.site/whatever")
data =
data
|> Map.put("object", object)
assert capture_log(fn ->
{:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
end) =~ "[error] Couldn't fetch \"https://404.site/whatever\", error: nil"
end
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object_data = Object.normalize(data["object"]).data
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object_data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert object_data["actor"] == "http://mastodon.example.org/users/admin"
assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object_data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object_data["sensitive"] == true
user = User.get_cached_by_ap_id(object_data["actor"])
assert user.note_count == 1
end
test "it works for incoming notices with hashtags" do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert Enum.at(object.data["tag"], 2) == "moo"
end
test "it works for incoming questions" do
data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity)
assert Enum.all?(object.data["oneOf"], fn choice ->
choice["name"] in [
"Dunno",
"Everyone knows that!",
"25 char limit is dumb",
"I can't even fit a funny"
]
end)
end
test "it works for incoming listens" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Listen",
"id" => "http://mastodon.example.org/users/admin/listens/1234/activity",
"actor" => "http://mastodon.example.org/users/admin",
"object" => %{
"type" => "Audio",
"id" => "http://mastodon.example.org/users/admin/listens/1234",
"attributedTo" => "http://mastodon.example.org/users/admin",
"title" => "lain radio episode 1",
"artist" => "lain",
"album" => "lain radio",
"length" => 180_000
}
}
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity)
assert object.data["title"] == "lain radio episode 1"
assert object.data["artist"] == "lain"
assert object.data["album"] == "lain radio"
assert object.data["length"] == 180_000
end
test "it rewrites Note votes to Answers and increments vote counters on question activities" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "suya...",
"poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
})
object = Object.normalize(activity)
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Poison.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
answer_object = Object.normalize(activity)
assert answer_object.data["type"] == "Answer"
object = Object.get_by_ap_id(object.data["id"])
assert Enum.any?(
object.data["oneOf"],
fn
%{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
_ -> false
end
)
end
test "it works for incoming notices with contentMap" do
data =
File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["content"] ==
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
test "it works for incoming notices with to/cc not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["content"] ==
"<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
end
test "it works for incoming announces with actor being inlined (kroeg)" do
data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://puckipedia.com/"
end
test "it works for incoming notices with tag not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["emoji"] == %{
"icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
}
data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert "test" in object.data["tag"]
end
test "it works for incoming notices with url not being a string (prismo)" do
data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["url"] == "https://prismo.news/posts/83"
end
test "it cleans up incoming notices which are not really DMs" do
user = insert(:user)
other_user = insert(:user)
to = [user.ap_id, other_user.ap_id]
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("to", to)
|> Map.put("cc", [])
object =
data["object"]
|> Map.put("to", to)
|> Map.put("cc", [])
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
assert data["to"] == []
assert data["cc"] == to
object_data = Object.normalize(activity).data
assert object_data["to"] == []
assert object_data["cc"] == to
end
- test "it works for incoming likes" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/mastodon-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
-
- refute Enum.empty?(activity.recipients)
-
- assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "Like"
- assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
- assert data["object"] == activity.data["object"]
- end
-
- test "it works for incoming misskey likes, turning them into EmojiReacts" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/misskey-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReact"
- assert data["id"] == data["id"]
- assert data["object"] == activity.data["object"]
- assert data["content"] == "🍮"
- end
-
- test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/misskey-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
- |> Map.put("_misskey_reaction", "⭐")
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReact"
- assert data["id"] == data["id"]
- assert data["object"] == activity.data["object"]
- assert data["content"] == "⭐"
- end
-
test "it works for incoming emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "EmojiReact"
assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
assert data["object"] == activity.data["object"]
assert data["content"] == "👌"
end
test "it reject invalid emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction-too-long.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert :error = Transmogrifier.handle_incoming(data)
data =
File.read!("test/fixtures/emoji-reaction-no-emoji.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert :error = Transmogrifier.handle_incoming(data)
end
test "it works for incoming emoji reaction undos" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
{:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", reaction_activity.data["id"])
|> Map.put("actor", user.ap_id)
{:ok, activity} = Transmogrifier.handle_incoming(data)
assert activity.actor == user.ap_id
assert activity.data["id"] == data["id"]
assert activity.data["type"] == "Undo"
end
test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert Transmogrifier.handle_incoming(data) == :error
end
test "it works for incoming unlikes with an existing like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data)
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming unlikes with an existing like activity and a compact object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data["id"])
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] ==
"http://mastodon.example.org/users/admin/statuses/99541947525187367"
assert Activity.get_create_by_object_ap_id(data["object"])
end
test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == activity.data["object"]
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
end
test "it works for incoming announces with an inlined activity" do
data =
File.read!("test/fixtures/mastodon-announce-private.json")
|> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
object = Object.normalize(data["object"])
assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
assert object.data["content"] == "this is a private toot"
end
@tag capture_log: true
test "it rejects incoming announces with an inlined activity from another origin" do
data =
File.read!("test/fixtures/bogus-mastodon-announce.json")
|> Poison.decode!()
assert :error = Transmogrifier.handle_incoming(data)
end
test "it does not clobber the addressing on announce activities" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", Object.normalize(activity).data["id"])
|> Map.put("to", ["http://mastodon.example.org/users/admin/followers"])
|> Map.put("cc", [])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["to"] == ["http://mastodon.example.org/users/admin/followers"]
end
test "it ensures that as:Public activities make it to their followers collection" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["cc"] == [User.ap_followers(user)]
end
test "it ensures that address fields become lists" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("to", nil)
|> Map.put("cc", nil)
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("to", nil)
|> Map.put("cc", nil)
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert !is_nil(data["to"])
assert !is_nil(data["cc"])
end
test "it strips internal likes" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
likes = %{
"first" =>
"http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
"id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
"totalItems" => 3,
"type" => "OrderedCollection"
}
object = Map.put(data["object"], "likes", likes)
data = Map.put(data, "object", object)
{:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data)
refute Map.has_key?(object.data, "likes")
end
test "it strips internal reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
%{object: object} = Activity.get_by_id_with_object(activity.id)
assert Map.has_key?(object.data, "reactions")
assert Map.has_key?(object.data, "reaction_count")
object_data = Transmogrifier.strip_internal_fields(object.data)
refute Map.has_key?(object_data, "reactions")
refute Map.has_key?(object_data, "reaction_count")
end
test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
assert data["id"] == update_data["id"]
user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle"
assert user.avatar["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
assert user.banner["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
assert user.bio == "<p>Some bio</p>"
end
test "it works with alsoKnownAs" do
{:ok, %Activity{data: %{"actor" => actor}}} =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Poison.decode!()
|> Transmogrifier.handle_incoming()
assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"]
{:ok, _activity} =
"test/fixtures/mastodon-update.json"
|> File.read!()
|> Poison.decode!()
|> Map.put("actor", actor)
|> Map.update!("object", fn object ->
object
|> Map.put("actor", actor)
|> Map.put("id", actor)
|> Map.put("alsoKnownAs", [
"http://mastodon.example.org/users/foo",
"http://example.org/users/bar"
])
end)
|> Transmogrifier.handle_incoming()
assert User.get_cached_by_ap_id(actor).also_known_as == [
"http://mastodon.example.org/users/foo",
"http://example.org/users/bar"
]
end
test "it works with custom profile fields" do
{:ok, activity} =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Poison.decode!()
|> Transmogrifier.handle_incoming()
user = User.get_cached_by_ap_id(activity.actor)
assert user.fields == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "foo1", "value" => "bar1"}
]
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
{:ok, _update_activity} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.fields == [
%{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"}
]
Pleroma.Config.put([:instance, :max_remote_account_fields], 2)
update_data =
put_in(update_data, ["object", "attachment"], [
%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"},
%{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"},
%{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"}
])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.fields == [
%{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"}
]
update_data = put_in(update_data, ["object", "attachment"], [])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.fields == []
end
test "it works for incoming update activities which lock the account" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
|> Map.put("manuallyApprovesFollowers", true)
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"])
assert user.locked == true
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
deleting_user = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
|> Map.put("actor", deleting_user.ap_id)
{:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
Transmogrifier.handle_incoming(data)
assert id == data["id"]
refute Activity.get_by_id(activity.id)
assert actor == deleting_user.ap_id
end
test "it fails for incoming deletes with spoofed origin" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~
"[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id)
end
@tag capture_log: true
test "it works for incoming user deletes" do
%{ap_id: ap_id} =
insert(:user, ap_id: "http://mastodon.example.org/users/admin", local: false)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
{:ok, _} = Transmogrifier.handle_incoming(data)
ObanHelpers.perform_all()
refute User.get_cached_by_ap_id(ap_id)
end
test "it fails for incoming user deletes with spoofed origin" do
%{ap_id: ap_id} = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
|> Map.put("actor", ap_id)
assert capture_log(fn ->
assert :error == Transmogrifier.handle_incoming(data)
end) =~ "Object containment failed"
assert User.get_cached_by_ap_id(ap_id)
end
test "it works for incoming unannounces with an existing notice" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: announce_data, local: false}} =
Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-undo-announce.json")
|> Poison.decode!()
|> Map.put("object", announce_data)
|> Map.put("actor", announce_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert object_data = data["object"]
assert object_data["type"] == "Announce"
assert object_data["object"] == activity.data["object"]
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end
test "it works for incomming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Poison.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it works for incoming follows to locked account" do
pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
user = insert(:user, locked: true)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Follow"
assert data["object"] == user.ap_id
assert data["state"] == "pending"
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert [^pending_follower] = User.get_follow_requests(user)
end
test "it works for incoming blocks" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
assert User.blocks?(blocker, user)
end
test "incoming blocks successfully tear down any follow relationship" do
blocker = insert(:user)
blocked = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", blocked.ap_id)
|> Map.put("actor", blocker.ap_id)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == blocked.ap_id
assert data["actor"] == blocker.ap_id
blocker = User.get_cached_by_ap_id(data["actor"])
blocked = User.get_cached_by_ap_id(data["object"])
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Block"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user)
followed = insert(:user)
{:ok, follower} = User.follow(follower, followed)
assert User.following?(follower, followed) == true
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
object =
accept_data["object"]
|> Map.put("actor", follower.ap_id)
|> Map.put("id", follow_activity.data["id"])
accept_data = Map.put(accept_data, "object", object)
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
refute activity.local
assert activity.data["object"] == follow_activity.data["id"]
assert activity.data["id"] == accept_data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which were orphaned" do
follower = insert(:user)
followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user)
followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it fails for incoming accepts which cannot be correlated" do
follower = insert(:user)
followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, followed) == true
end
test "it fails for incoming rejects which cannot be correlated" do
follower = insert(:user)
followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, followed) == true
end
test "it works for incoming rejects which are orphaned" do
follower = insert(:user)
followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, _follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
reject_data =
Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(reject_data)
refute activity.local
assert activity.data["id"] == reject_data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == false
end
test "it works for incoming rejects which are referenced by IRI only" do
follower = insert(:user)
followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == false
end
test "it rejects activities without a valid ID" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
|> Map.put("id", "")
:error = Transmogrifier.handle_incoming(data)
end
test "skip converting the content when it is nil" do
object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
{:ok, object} = Fetcher.fetch_and_contain_remote_object_from_id(object_id)
result =
Pleroma.Web.ActivityPub.Transmogrifier.fix_object(Map.merge(object, %{"content" => nil}))
assert result["content"] == nil
end
test "it converts content of object to html" do
object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
{:ok, %{"content" => content_markdown}} =
Fetcher.fetch_and_contain_remote_object_from_id(object_id)
{:ok, %Pleroma.Object{data: %{"content" => content}} = object} =
Fetcher.fetch_object_from_id(object_id)
assert content_markdown ==
"Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've..."
assert content ==
"<p>Support this and our other Michigan!/usr/group videos and meetings. Learn more at <a href=\"http://mug.org/membership\">http://mug.org/membership</a></p><p>Twenty Years in Jail: FreeBSD’s Jails, Then and Now</p><p>Jails started as a limited virtualization system, but over the last two years they’ve…</p>"
assert object.data["mediaType"] == "text/html"
end
test "it remaps video URLs as attachments if necessary" do
{:ok, object} =
Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
attachment = %{
"type" => "Link",
"mediaType" => "video/mp4",
"url" => [
%{
"href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mediaType" => "video/mp4"
}
]
}
assert object.data["url"] ==
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
assert object.data["attachment"] == [attachment]
end
test "it accepts Flag activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
object = Object.normalize(activity)
note_obj = %{
"type" => "Note",
"id" => activity.data["id"],
"content" => "test post",
"published" => object.data["published"],
"actor" => AccountView.render("show.json", %{user: user})
}
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => [user.ap_id, activity.data["id"]],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["object"] == [user.ap_id, note_obj]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
test "it correctly processes messages with non-array to field" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => "https://www.w3.org/ns/activitystreams#Public",
"type" => "Create",
"object" => %{
"content" => "blah blah blah",
"type" => "Note",
"attributedTo" => user.ap_id,
"inReplyTo" => nil
},
"actor" => user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
end
test "it correctly processes messages with non-array cc field" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => user.follower_address,
"cc" => "https://www.w3.org/ns/activitystreams#Public",
"type" => "Create",
"object" => %{
"content" => "blah blah blah",
"type" => "Note",
"attributedTo" => user.ap_id,
"inReplyTo" => nil
},
"actor" => user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
assert [user.follower_address] == activity.data["to"]
end
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Move",
"actor" => old_user.ap_id,
"object" => old_user.ap_id,
"target" => new_user.ap_id
}
assert :error = Transmogrifier.handle_incoming(message)
{:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
assert activity.actor == old_user.ap_id
assert activity.data["actor"] == old_user.ap_id
assert activity.data["object"] == old_user.ap_id
assert activity.data["target"] == new_user.ap_id
assert activity.data["type"] == "Move"
end
end
describe "`handle_incoming/2`, Mastodon format `replies` handling" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Poison.decode!()
items = get_in(data, ["object", "replies", "first", "items"])
assert length(items) > 0
%{data: data, items: items}
end
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
data: data,
items: items
} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
{:ok, _activity} = Transmogrifier.handle_incoming(data)
for id <- items do
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
end
end
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
%{data: data} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
{:ok, _activity} = Transmogrifier.handle_incoming(data)
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
end
end
describe "`handle_incoming/2`, Pleroma format `replies` handling" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "post1"})
{:ok, reply1} =
CommonAPI.post(user, %{"status" => "reply1", "in_reply_to_status_id" => activity.id})
{:ok, reply2} =
CommonAPI.post(user, %{"status" => "reply2", "in_reply_to_status_id" => activity.id})
replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end)
{:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data)
Repo.delete(activity.object)
Repo.delete(activity)
%{federation_output: federation_output, replies_uris: replies_uris}
end
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
federation_output: federation_output,
replies_uris: replies_uris
} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1)
{:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
for id <- replies_uris do
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
end
end
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
%{federation_output: federation_output} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
{:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
end
end
describe "prepare outgoing" do
test "it inlines private announced objects" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey", "visibility" => "private"})
{:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
{:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
assert modified["object"]["content"] == "hey"
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
test "it adds the sensitive property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["sensitive"]
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["@context"] ==
Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it strips internal hashtag data" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu"})
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["tag"] == [expected_tag]
end
test "it strips internal fields" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu :firefox:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["context_id"])
end
test "it strips internal fields of article" do
activity = insert(:article_activity)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["context_id"])
assert is_nil(modified["object"]["likes"])
end
test "the directMessage flag is present" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} :moominmamma:",
"visibility" => "direct"
})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == true
end
test "it strips BCC field" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert is_nil(modified["bcc"])
end
test "it can handle Listen activities" do
listen_activity = insert(:listen)
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
assert modified["type"] == "Listen"
user = insert(:user)
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
{:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
end
end
describe "user upgrade" do
test "it upgrades a user to activitypub" do
user =
insert(:user, %{
nickname: "rye@niu.moe",
local: false,
ap_id: "https://niu.moe/users/rye",
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
user_two = insert(:user)
Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = User.get_cached_by_id(user.id)
assert user.note_count == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
ObanHelpers.perform_all()
assert user.ap_enabled
assert user.note_count == 1
assert user.follower_address == "https://niu.moe/users/rye/followers"
assert user.following_address == "https://niu.moe/users/rye/following"
user = User.get_cached_by_id(user.id)
assert user.note_count == 1
activity = Activity.get_by_id(activity.id)
assert user.follower_address in activity.recipients
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
} = user.avatar
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
} = user.banner
refute "..." in activity.recipients
unrelated_activity = Activity.get_by_id(unrelated_activity.id)
refute user.follower_address in unrelated_activity.recipients
user_two = User.get_cached_by_id(user_two.id)
assert User.following?(user_two, user)
refute "..." in User.following(user_two)
end
end
describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{
"url" => %{"href" => "http://example.com"}
}
rewritten = Transmogrifier.maybe_fix_user_object(data)
assert rewritten["url"] == "http://example.com"
end
end
describe "actor origin containment" do
test "it rejects activities which reference objects with bogus origins" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity.json",
"type" => "Announce"
}
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~ "Object containment failed"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity2.json",
"type" => "Announce"
}
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~ "Object containment failed"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity3.json",
"type" => "Announce"
}
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~ "Object containment failed"
end
end
describe "reserialization" do
test "successfully reserializes a message with inReplyTo == nil" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
end
test "successfully reserializes a message with AS2 objects in IR" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id,
"tag" => [
%{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
%{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
]
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
end
end
test "Rewrites Answers to Notes" do
user = insert(:user)
{:ok, poll_activity} =
CommonAPI.post(user, %{
"status" => "suya...",
"poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
})
poll_object = Object.normalize(poll_activity)
# TODO: Replace with CommonAPI vote creation when implemented
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Poison.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
assert data["object"]["type"] == "Note"
end
describe "fix_explicit_addressing" do
setup do
user = insert(:user)
[user: user]
end
test "moves non-explicitly mentioned actors to cc", %{user: user} do
explicitly_mentioned_actors = [
"https://pleroma.gold/users/user1",
"https://pleroma.gold/user2"
]
object = %{
"actor" => user.ap_id,
"to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
"cc" => [],
"tag" =>
Enum.map(explicitly_mentioned_actors, fn href ->
%{"type" => "Mention", "href" => href}
end)
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
end
test "does not move actor's follower collection to cc", %{user: user} do
object = %{
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => []
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert user.follower_address in fixed_object["to"]
refute user.follower_address in fixed_object["cc"]
end
test "removes recipient's follower collection from cc", %{user: user} do
recipient = insert(:user)
object = %{
"actor" => user.ap_id,
"to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address, recipient.follower_address]
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert user.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["to"]
end
end
describe "fix_summary/1" do
test "returns fixed object" do
assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
end
end
describe "fix_in_reply_to/2" do
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
[data: data]
end
test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
assert Transmogrifier.fix_in_reply_to(data) == data
end
test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
object_with_reply =
Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyToAtomUri"] == ""
end
@tag capture_log: true
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(
data["object"],
"inReplyTo",
"https://shitposter.club/notice/2827873"
)
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
assert modified_object["conversation"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
assert modified_object["context"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
end
end
describe "fix_url/1" do
test "fixes data for object when url is map" do
object = %{
"url" => %{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
}
assert Transmogrifier.fix_url(object) == %{
"url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
end
test "fixes data for video object" do
object = %{
"type" => "Video",
"url" => [
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
},
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"attachment" => [
%{
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mimeType" => "video/mp4",
"type" => "Link"
}
],
"type" => "Video",
"url" => "https://peertube.-2d4c2d1630e3"
}
end
test "fixes url for not Video object" do
object = %{
"type" => "Text",
"url" => [
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"type" => "Text",
"url" => "https://peertube.-2d4c2d1630e3"
}
assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
"type" => "Text",
"url" => ""
}
end
test "retunrs not modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
describe "get_obj_helper/2" do
test "returns nil when cannot normalize object" do
assert capture_log(fn ->
refute Transmogrifier.get_obj_helper("test-obj-id")
end) =~ "Unsupported URI scheme"
end
@tag capture_log: true
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
end
end
describe "fix_attachments/1" do
test "returns not modified object" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_attachments(data) == data
end
test "returns modified object when attachment is map" do
assert Transmogrifier.fix_attachments(%{
"attachment" => %{
"mediaType" => "video/mp4",
"url" => "https://peertube.moe/stat-480.mp4"
}
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{"href" => "https://peertube.moe/stat-480.mp4", "mediaType" => "video/mp4"}
]
}
]
}
end
test "returns modified object when attachment is list" do
assert Transmogrifier.fix_attachments(%{
"attachment" => [
%{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
%{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
]
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
]
},
%{
"mediaType" => "video/mp4",
"url" => [
%{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
]
}
]
}
end
end
describe "fix_emoji/1" do
test "returns not modified object when object not contains tags" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_emoji(data) == data
end
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{"type" => "Hashtag"}
]
}
end
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
}
end
end
describe "set_replies/1" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
test "returns unmodified object if activity doesn't have self-replies" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.set_replies(data) == data
end
test "sets `replies` collection with a limited number of self-replies" do
[user, another_user] = insert_list(2, :user)
{:ok, %{id: id1} = activity} = CommonAPI.post(user, %{"status" => "1"})
{:ok, %{id: id2} = self_reply1} =
CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => id1})
{:ok, self_reply2} =
CommonAPI.post(user, %{"status" => "self-reply 2", "in_reply_to_status_id" => id1})
# Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
{:ok, _} =
CommonAPI.post(user, %{"status" => "self-reply 3", "in_reply_to_status_id" => id1})
{:ok, _} =
CommonAPI.post(user, %{
"status" => "self-reply to self-reply",
"in_reply_to_status_id" => id2
})
{:ok, _} =
CommonAPI.post(another_user, %{
"status" => "another user's reply",
"in_reply_to_status_id" => id1
})
object = Object.normalize(activity)
replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
assert %{"type" => "Collection", "items" => ^replies_uris} =
Transmogrifier.set_replies(object.data)["replies"]
end
end
test "take_emoji_tags/1" do
user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}})
assert Transmogrifier.take_emoji_tags(user) == [
%{
"icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
"id" => "https://example.org/firefox.png",
"name" => ":firefox:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
}
]
end
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 1862a9589..c3f3ad051 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,3712 +1,3731 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true)
test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false)
test "GET /api/pleroma/admin/users/:nickname requires " <>
"read:accounts or admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
good_token5 = insert(:oauth_token, user: admin, scopes: ["read"])
good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5]
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "DELETE /api/pleroma/admin/users" do
test "single user", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == user.nickname
end
test "multiple users", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users", %{
nicknames: [user_one.nickname, user_two.nickname]
})
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
response = json_response(conn, 200)
assert response -- [user_one.nickname, user_two.nickname] == []
end
end
describe "/api/pleroma/admin/users" do
test "Create", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
})
response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
assert response == ["success", "success"]
log_entry = Repo.one(ModerationLog)
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
}
]
end
test "Cannot create user with existing nickname", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => "someuser@plerama.social",
"nickname" => user.nickname
},
"error" => "nickname has already been taken",
"type" => "error"
}
]
end
test "Multiple user creation works in transaction", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
},
%{
"code" => 409,
"data" => %{
"email" => "newuser@pleroma.social",
"nickname" => "newuser"
},
"error" => "",
"type" => "error"
}
]
assert User.get_by_nickname("newuser") === nil
end
end
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
expected = %{
"deactivated" => false,
"id" => to_string(user.id),
"local" => true,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
assert expected == json_response(conn, 200)
end
test "when the user doesn't exist", %{conn: conn} do
user = build(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert "Not found" == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
test "allows to force-follow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end
end
describe "/api/pleroma/admin/users/unfollow" do
test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end
end
describe "PUT /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
conn
|> put_req_header("accept", "application/json")
|> put(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
"#{user2.nickname}&tags[]=foo&tags[]=bar"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it appends specified tags to users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "DELETE /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
conn
|> put_req_header("accept", "application/json")
|> delete(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
"#{user2.nickname}&tags[]=x&tags[]=z"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it removes specified tags from users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["x", "z"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
test "GET is giving user_info", %{admin: admin, conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
assert json_response(conn, 200) == %{
"is_admin" => true,
"is_moderator" => false
}
end
test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => true
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user.nickname} admin"
end
test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
assert json_response(conn, 200) == %{"is_admin" => true}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do
user = insert(:user, is_admin: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
test "/:right DELETE, can remove from a permission group (multiple)", %{
admin: admin,
conn: conn
} do
user_one = insert(:user, is_admin: true)
user_two = insert(:user, is_admin: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
user_two.nickname
}"
end
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
setup do: clear_config([:instance, :registrations_open], false)
setup do: clear_config([:instance, :invites_enabled], true)
test "sends invitation and returns 204", %{admin: admin, conn: conn} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
post(
conn,
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)
assert json_response(conn, :no_content)
token_record = List.last(Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
admin,
token_record,
recipient_email,
recipient_name
)
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {recipient_name, recipient_email},
html_body: email.html_body
)
end
test "it returns 403 if requested by a non-admin" do
non_admin_user = insert(:user)
token = insert(:oauth_token, user: non_admin_user)
conn =
build_conn()
|> assign(:user, non_admin_user)
|> assign(:token, token)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
end
test "email with +", %{conn: conn, admin: admin} do
recipient_email = "foo+bar@baz.com"
conn
|> put_req_header("content-type", "application/json;charset=utf-8")
|> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email})
|> json_response(:no_content)
token_record =
Pleroma.UserInviteToken
|> Repo.all()
|> List.last()
assert token_record
refute token_record.used
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
admin,
token_record,
recipient_email
)
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: recipient_email,
html_body: email.html_body
)
end
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
setup do: clear_config([:instance, :registrations_open])
setup do: clear_config([:instance, :invites_enabled])
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
Config.put([:instance, :registrations_open], false)
Config.put([:instance, :invites_enabled], false)
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :bad_request) ==
"To send invites you need to set the `invites_enabled` option to true."
end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
Config.put([:instance, :registrations_open], true)
Config.put([:instance, :invites_enabled], true)
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :bad_request) ==
"To send invites you need to set the `registrations_open` option to false."
end
end
test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
resp = json_response(conn, 200)
assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "GET /api/pleroma/admin/users" do
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
conn = get(conn, "/api/pleroma/admin/users?page=1")
users =
[
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => false,
"tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?page=2")
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => []
}
end
test "regular search", %{conn: conn} do
user = insert(:user, nickname: "bob")
conn = get(conn, "/api/pleroma/admin/users?query=bo")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by domain", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by full nickname", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by display name", %{conn: conn} do
user = insert(:user, name: "Display name")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?name=display")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by email", %{conn: conn} do
user = insert(:user, email: "email@example.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?email=email@example.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "regular search with page size", %{conn: conn} do
user = insert(:user, nickname: "aalice")
user2 = insert(:user, nickname: "alice")
conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1")
assert json_response(conn1, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2")
assert json_response(conn2, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
"deactivated" => user2.deactivated,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
}
end
test "only local users" do
admin = insert(:user, is_admin: true, nickname: "john")
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "only local users with no query", %{conn: conn, admin: old_admin} do
admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn = get(conn, "/api/pleroma/admin/users?filters=local")
users =
[
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => old_admin.id,
"local" => true,
"nickname" => old_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "load only admins", %{conn: conn, admin: admin} do
second_admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_admin")
users =
[
%{
"deactivated" => false,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => admin.local,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => second_admin.id,
"nickname" => second_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => second_admin.local,
"tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "load only moderators", %{conn: conn} do
moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => false,
"id" => moderator.id,
"nickname" => moderator.nickname,
"roles" => %{"admin" => false, "moderator" => true},
"local" => moderator.local,
"tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
"confirmation_pending" => false
}
]
}
end
test "load users with tags list", %{conn: conn} do
user1 = insert(:user, tags: ["first"])
user2 = insert(:user, tags: ["second"])
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second")
users =
[
%{
"deactivated" => false,
"id" => user1.id,
"nickname" => user1.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user1.local,
"tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user1.name || user1.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user2.local,
"tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "it works with multiple filters" do
admin = insert(:user, nickname: "john", is_admin: true)
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob", local: false, deactivated: true)
insert(:user, nickname: "ken", local: true, deactivated: true)
insert(:user, nickname: "bobb", local: false, deactivated: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "it omits relay user", %{admin: admin, conn: conn} do
assert %User{} = Relay.get_actor()
conn = get(conn, "/api/pleroma/admin/users")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}
]
}
end
end
test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: true)
user_two = insert(:user, deactivated: true)
conn =
patch(
conn,
"/api/pleroma/admin/users/activate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: false)
user_two = insert(:user, deactivated: false)
conn =
patch(
conn,
"/api/pleroma/admin/users/deactivate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user)
conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
%{
"deactivated" => !user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
describe "POST /api/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
refute invite.max_use
assert invite.invite_type == "one_time"
end
test "with expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
refute invite.max_use
assert invite.invite_type == "date_limited"
end
test "with max_use", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
assert invite.max_use == 150
assert invite.invite_type == "reusable"
end
test "with max use and expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"max_use" => 150,
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
assert invite.max_use == 150
assert invite.invite_type == "reusable_date_limited"
end
end
describe "GET /api/pleroma/admin/users/invites" do
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{"invites" => []}
end
test "with invite", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{
"invites" => [
%{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => false,
"uses" => 0
}
]
}
end
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => true,
"uses" => 0
}
end
test "with invalid token", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports/:id" do
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports/#{report_id}")
|> json_response(:ok)
assert response["id"] == report_id
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/reports/test")
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PATCH /api/pleroma/admin/reports" do
setup do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel very offended",
status_ids: [activity.id]
})
%{
id: report_id,
second_report_id: second_report_id
}
end
test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do
read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"])
write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"])
response =
conn
|> assign(:token, read_token)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(403)
assert response == %{
"error" => "Insufficient permissions: admin:write:reports."
}
conn
|> assign(:token, write_token)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(:no_content)
end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
assert activity.data["state"] == "resolved"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
end
test "closes report", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
assert activity.data["state"] == "closed"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'closed' state"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})
assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})
assert hd(json_response(conn, :bad_request))["error"] == "not_found"
end
test "updates state of multiple reports", %{
conn: conn,
id: id,
admin: admin,
second_report_id: second_report_id
} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
second_activity = Activity.get_by_id(second_report_id)
assert activity.data["state"] == "resolved"
assert second_activity.data["state"] == "closed"
[first_log_entry, second_log_entry] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(first_log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
end
end
describe "GET /api/pleroma/admin/reports" do
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns reports", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
[report] = response["reports"]
assert length(response["reports"]) == 1
assert report["id"] == report_id
assert response["total"] == 1
end
test "returns reports with specified state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I don't like this user"
})
CommonAPI.update_report_state(second_report_id, "closed")
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "open"
})
|> json_response(:ok)
[open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "closed"
})
|> json_response(:ok)
[closed_report] = response["reports"]
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
token = insert(:oauth_token, user: user)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, token)
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) ==
%{"error" => "User is not an admin or OAuth admin scope is not granted."}
end
test "returns 403 when requested by anonymous" do
conn = get(build_conn(), "/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
+ describe "GET /api/pleroma/admin/statuses/:id" do
+ test "not found", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/admin/statuses/not_found")
+ |> json_response(:not_found)
+ end
+
+ test "shows activity", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/statuses/#{activity.id}")
+ |> json_response(200)
+
+ assert response["id"] == activity.id
+ end
+ end
+
describe "PUT /api/pleroma/admin/statuses/:id" do
setup do
activity = insert(:note_activity)
%{id: activity.id}
end
test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response(:ok)
assert response["sensitive"]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set sensitive: 'true'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|> json_response(:ok)
refute response["sensitive"]
end
test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|> json_response(:ok)
assert response["visibility"] == "public"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set visibility: 'public'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
|> json_response(:ok)
assert response["visibility"] == "private"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"})
|> json_response(:ok)
assert response["visibility"] == "unlisted"
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
conn = put(conn, "/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
setup do
activity = insert(:note_activity)
%{id: activity.id}
end
test "deletes status", %{conn: conn, id: id, admin: admin} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted status ##{id}"
end
test "returns 404 when the status does not exist", %{conn: conn} do
conn = delete(conn, "/api/pleroma/admin/statuses/test")
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/config" do
setup do: clear_config(:configurable_from_database, true)
test "when configuration from database is off", %{conn: conn} do
Config.put(:configurable_from_database, false)
conn = get(conn, "/api/pleroma/admin/config")
assert json_response(conn, 400) ==
"To use this endpoint you need to enable configuration from database."
end
test "with settings only in db", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true})
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => key1,
"value" => _
},
%{
"group" => ":pleroma",
"key" => key2,
"value" => _
}
]
} = json_response(conn, 200)
assert key1 == config1.key
assert key2 == config2.key
end
test "db is added to settings that are in db", %{conn: conn} do
_config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name"))
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
[instance_config] =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key == ":instance"
end)
assert instance_config["db"] == [":name"]
end
test "merged default setting with db settings", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
config3 =
insert(:config,
value: ConfigDB.to_binary(k1: :v1, k2: :v2)
)
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
assert length(configs) > 3
received_configs =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key in [config1.key, config2.key, config3.key]
end)
assert length(received_configs) == 3
db_keys =
config3.value
|> ConfigDB.from_binary()
|> Keyword.keys()
|> ConfigDB.convert()
Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
assert db in [[config1.key], [config2.key], db_keys]
assert value in [
ConfigDB.from_binary_with_convert(config1.value),
ConfigDB.from_binary_with_convert(config2.value),
ConfigDB.from_binary_with_convert(config3.value)
]
end)
end
test "subkeys with full update right merge", %{conn: conn} do
config1 =
insert(:config,
key: ":emoji",
value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
)
config2 =
insert(:config,
key: ":assets",
value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
)
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
vals =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key in [config1.key, config2.key]
end)
emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
emoji_val = ConfigDB.transform_with_out_binary(emoji["value"])
assets_val = ConfigDB.transform_with_out_binary(assets["value"])
assert emoji_val[:groups] == [a: 1, b: 2]
assert assets_val[:mascots] == [a: 1, b: 2]
end
end
test "POST /api/pleroma/admin/config error", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []})
assert json_response(conn, 400) ==
"To use this endpoint you need to enable configuration from database."
end
describe "POST /api/pleroma/admin/config" do
setup do
http = Application.get_env(:pleroma, :http)
on_exit(fn ->
Application.delete_env(:pleroma, :key1)
Application.delete_env(:pleroma, :key2)
Application.delete_env(:pleroma, :key3)
Application.delete_env(:pleroma, :key4)
Application.delete_env(:pleroma, :keyaa1)
Application.delete_env(:pleroma, :keyaa2)
Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
Application.put_env(:pleroma, :http, http)
Application.put_env(:tesla, :adapter, Tesla.Mock)
Restarter.Pleroma.refresh()
end)
end
setup do: clear_config(:configurable_from_database, true)
@tag capture_log: true
test "create new config setting in db", %{conn: conn} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: ":pleroma", key: ":key1", value: "value1"},
%{
group: ":ueberauth",
key: "Ueberauth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
group: ":pleroma",
key: ":key2",
value: %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
}
},
%{
group: ":pleroma",
key: ":key3",
value: [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
group: ":pleroma",
key: ":key4",
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
group: ":idna",
key: ":key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => "value1",
"db" => [":key1"]
},
%{
"group" => ":ueberauth",
"key" => "Ueberauth",
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
"db" => [":consumer_secret"]
},
%{
"group" => ":pleroma",
"key" => ":key2",
"value" => %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
},
"db" => [":key2"]
},
%{
"group" => ":pleroma",
"key" => ":key3",
"value" => [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
],
"db" => [":key3"]
},
%{
"group" => ":pleroma",
"key" => ":key4",
"value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
"db" => [":key4"]
},
%{
"group" => ":idna",
"key" => ":key5",
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
"db" => [":key5"]
}
]
}
assert Application.get_env(:pleroma, :key1) == "value1"
assert Application.get_env(:pleroma, :key2) == %{
nested_1: "nested_value1",
nested_2: [
%{nested_22: "nested_value222"},
%{nested_33: %{nested_44: "nested_444"}}
]
}
assert Application.get_env(:pleroma, :key3) == [
%{"nested_3" => :nested_3, "nested_33" => "nested_33"},
%{"nested_4" => true}
]
assert Application.get_env(:pleroma, :key4) == %{
"endpoint" => "https://example.com",
nested_5: :upload
}
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
test "save configs setting without explicit key", %{conn: conn} do
level = Application.get_env(:quack, :level)
meta = Application.get_env(:quack, :meta)
webhook_url = Application.get_env(:quack, :webhook_url)
on_exit(fn ->
Application.put_env(:quack, :level, level)
Application.put_env(:quack, :meta, meta)
Application.put_env(:quack, :webhook_url, webhook_url)
end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":quack",
key: ":level",
value: ":info"
},
%{
group: ":quack",
key: ":meta",
value: [":none"]
},
%{
group: ":quack",
key: ":webhook_url",
value: "https://hooks.slack.com/services/KEY"
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":quack",
"key" => ":level",
"value" => ":info",
"db" => [":level"]
},
%{
"group" => ":quack",
"key" => ":meta",
"value" => [":none"],
"db" => [":meta"]
},
%{
"group" => ":quack",
"key" => ":webhook_url",
"value" => "https://hooks.slack.com/services/KEY",
"db" => [":webhook_url"]
}
]
}
assert Application.get_env(:quack, :level) == :info
assert Application.get_env(:quack, :meta) == [:none]
assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY"
end
test "saving config with partial update", %{conn: conn} do
config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key1", 1]},
%{"tuple" => [":key2", 2]},
%{"tuple" => [":key3", 3]}
],
"db" => [":key1", ":key2", ":key3"]
}
]
}
end
test "saving config which need pleroma reboot", %{conn: conn} do
chat = Config.get(:chat)
on_exit(fn -> Config.put(:chat, chat) end)
assert post(
conn,
"/api/pleroma/admin/config",
%{
configs: [
%{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
]
}
)
|> json_response(200) == %{
"configs" => [
%{
"db" => [":enabled"],
"group" => ":pleroma",
"key" => ":chat",
"value" => [%{"tuple" => [":enabled", true]}]
}
],
"need_reboot" => true
}
configs =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
assert configs["need_reboot"]
capture_log(fn ->
assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
end) =~ "pleroma restarted"
configs =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
assert configs["need_reboot"] == false
end
test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do
chat = Config.get(:chat)
on_exit(fn -> Config.put(:chat, chat) end)
assert post(
conn,
"/api/pleroma/admin/config",
%{
configs: [
%{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
]
}
)
|> json_response(200) == %{
"configs" => [
%{
"db" => [":enabled"],
"group" => ":pleroma",
"key" => ":chat",
"value" => [%{"tuple" => [":enabled", true]}]
}
],
"need_reboot" => true
}
assert post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]}
]
})
|> json_response(200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key3", 3]}
],
"db" => [":key3"]
}
],
"need_reboot" => true
}
capture_log(fn ->
assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
end) =~ "pleroma restarted"
configs =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
assert configs["need_reboot"] == false
end
test "saving config with nested merge", %{conn: conn} do
config =
insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
value: [
%{"tuple" => [":key3", 3]},
%{
"tuple" => [
":key2",
[
%{"tuple" => [":k2", 1]},
%{"tuple" => [":k3", 3]}
]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key1", 1]},
%{"tuple" => [":key3", 3]},
%{
"tuple" => [
":key2",
[
%{"tuple" => [":k1", 1]},
%{"tuple" => [":k2", 1]},
%{"tuple" => [":k3", 3]}
]
]
}
],
"db" => [":key1", ":key3", ":key2"]
}
]
}
end
test "saving special atoms", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{
"tuple" => [
":ssl_options",
[%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{
"tuple" => [
":ssl_options",
[%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
]
}
],
"db" => [":ssl_options"]
}
]
}
assert Application.get_env(:pleroma, :key1) == [
ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]]
]
end
test "saving full setting if value is in full_key_update list", %{conn: conn} do
backends = Application.get_env(:logger, :backends)
on_exit(fn -> Application.put_env(:logger, :backends, backends) end)
config =
insert(:config,
group: ":logger",
key: ":backends",
value: :erlang.term_to_binary([])
)
Pleroma.Config.TransferTask.load_and_update_env([], false)
assert Application.get_env(:logger, :backends) == []
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
value: [":console"]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":logger",
"key" => ":backends",
"value" => [
":console"
],
"db" => [":backends"]
}
]
}
assert Application.get_env(:logger, :backends) == [
:console
]
end
test "saving full setting if value is not keyword", %{conn: conn} do
config =
insert(:config,
group: ":tesla",
key: ":adapter",
value: :erlang.term_to_binary(Tesla.Adapter.Hackey)
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":tesla",
"key" => ":adapter",
"value" => "Tesla.Adapter.Httpc",
"db" => [":adapter"]
}
]
}
end
test "update config setting & delete with fallback to default value", %{
conn: conn,
admin: admin,
token: token
} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
config1 = insert(:config, key: ":keyaa1")
config2 = insert(:config, key: ":keyaa2")
config3 =
insert(:config,
group: ":ueberauth",
key: "Ueberauth"
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config1.group, key: config1.key, value: "another_value"},
%{group: config2.group, key: config2.key, value: "another_value"}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => config1.key,
"value" => "another_value",
"db" => [":keyaa1"]
},
%{
"group" => ":pleroma",
"key" => config2.key,
"value" => "another_value",
"db" => [":keyaa2"]
}
]
}
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
assert Application.get_env(:pleroma, :keyaa2) == "another_value"
assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> post("/api/pleroma/admin/config", %{
configs: [
%{group: config2.group, key: config2.key, delete: true},
%{
group: ":ueberauth",
key: "Ueberauth",
delete: true
}
]
})
assert json_response(conn, 200) == %{
"configs" => []
}
assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
end
test "common config example", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]},
%{"tuple" => [":name", "Pleroma"]}
]
}
]
})
assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]},
%{"tuple" => [":name", "Pleroma"]}
],
"db" => [
":enabled",
":method",
":seconds_valid",
":path",
":key1",
":regex1",
":regex2",
":regex3",
":regex4",
":name"
]
}
]
}
end
test "tuples with more than two values", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
],
"db" => [":http"]
}
]
}
end
test "settings with nesting map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000,
"nested" => %{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000
}
}
]
}
]
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0,
"nested" => %{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0
}
}
]
}
],
"db" => [":key2", ":key3"]
}
]
}
end
test "value as map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"},
"db" => [":key1"]
}
]
}
end
test "queues key as atom", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
],
"db" => [
":federator_incoming",
":federator_outgoing",
":web_push",
":mailer",
":transmogrifier",
":scheduled_activities",
":background"
]
}
]
}
end
test "delete part of settings by atom subkeys", %{conn: conn} do
config =
insert(:config,
key: ":keyaa1",
value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
subkeys: [":subkey1", ":subkey3"],
delete: true
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":keyaa1",
"value" => [%{"tuple" => [":subkey2", "val2"]}],
"db" => [":subkey2"]
}
]
}
end
test "proxy tuple localhost", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
test "proxy tuple domain", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
test "proxy tuple ip", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
end
describe "GET /api/pleroma/admin/restart" do
setup do: clear_config(:configurable_from_database, true)
test "pleroma restarts", %{conn: conn} do
capture_log(fn ->
assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
end) =~ "pleroma restarted"
refute Restarter.Pleroma.need_reboot?()
end
end
test "need_reboot flag", %{conn: conn} do
assert conn
|> get("/api/pleroma/admin/need_reboot")
|> json_response(200) == %{"need_reboot" => false}
Restarter.Pleroma.need_reboot()
assert conn
|> get("/api/pleroma/admin/need_reboot")
|> json_response(200) == %{"need_reboot" => true}
on_exit(fn -> Restarter.Pleroma.refresh() end)
end
describe "GET /api/pleroma/admin/statuses" do
test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do
blocked = insert(:user)
user = insert(:user)
User.block(admin, blocked)
{:ok, _} =
CommonAPI.post(user, %{"status" => "@#{admin.nickname}", "visibility" => "direct"})
{:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
{:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, _} = CommonAPI.post(blocked, %{"status" => ".", "visibility" => "public"})
response =
conn
|> get("/api/pleroma/admin/statuses")
|> json_response(200)
refute "private" in Enum.map(response, & &1["visibility"])
assert length(response) == 3
end
test "returns only local statuses with local_only on", %{conn: conn} do
user = insert(:user)
remote_user = insert(:user, local: false, nickname: "archaeme@archae.me")
insert(:note_activity, user: user, local: true)
insert(:note_activity, user: remote_user, local: false)
response =
conn
|> get("/api/pleroma/admin/statuses?local_only=true")
|> json_response(200)
assert length(response) == 1
end
test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do
user = insert(:user)
{:ok, _} =
CommonAPI.post(user, %{"status" => "@#{admin.nickname}", "visibility" => "direct"})
{:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
{:ok, _} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/statuses?godmode=true")
assert json_response(conn, 200) |> length() == 3
end
end
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
insert(:note_activity, user: user, published: date1)
insert(:note_activity, user: user, published: date2)
insert(:note_activity, user: user, published: date3)
%{user: user}
end
test "renders user's statuses", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 3
end
test "renders user's statuses with a limit", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2")
assert json_response(conn, 200) |> length() == 2
end
test "doesn't return private statuses by default", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 4
end
test "returns private statuses with godmode on", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
assert json_response(conn, 200) |> length() == 5
end
test "excludes reblogs by default", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "."})
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, other_user)
conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses")
assert json_response(conn_res, 200) |> length() == 0
conn_res =
get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true")
assert json_response(conn_res, 200) |> length() == 1
end
end
describe "GET /api/pleroma/admin/moderation_log" do
setup do
moderator = insert(:user, is_moderator: true)
%{moderator: moderator}
end
test "returns the log", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn = get(conn, "/api/pleroma/admin/moderation_log")
response = json_response(conn, 200)
[first_entry, second_entry] = response["items"]
assert response["total"] == 2
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "returns the log with pagination", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 2
assert response1["items"] |> length() == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
response2 = json_response(conn2, 200)
[second_entry] = response2["items"]
assert response2["total"] == 2
assert response2["items"] |> length() == 1
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "filters log by date", %{conn: conn, admin: admin} do
first_date = "2017-08-15T15:47:06Z"
second_date = "2017-08-20T15:47:06Z"
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(first_date)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(second_date)
})
conn1 =
get(
conn,
"/api/pleroma/admin/moderation_log?start_date=#{second_date}"
)
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
end
test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
}
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => moderator.id,
"nickname" => moderator.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
}
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id
end
test "returns log filtered by search", %{conn: conn, moderator: moderator} do
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_follow",
target: "https://example.org/relay"
})
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_unfollow",
target: "https://example.org/relay"
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "message"]) ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
end
describe "GET /users/:nickname/credentials" do
test "gets the user credentials", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials")
response = assert json_response(conn, 200)
assert response["email"] == user.email
end
test "returns 403 if requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/admin/users/#{user.nickname}/credentials")
assert json_response(conn, :forbidden)
end
end
describe "PATCH /users/:nickname/credentials" do
test "changes password and email", %{conn: conn, admin: admin} do
user = insert(:user)
assert user.password_reset_pending == false
conn =
patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
"password" => "new_password",
"email" => "new_email@example.com",
"name" => "new_name"
})
assert json_response(conn, 200) == %{"status" => "success"}
ObanHelpers.perform_all()
updated_user = User.get_by_id(user.id)
assert updated_user.email == "new_email@example.com"
assert updated_user.name == "new_name"
assert updated_user.password_hash != user.password_hash
assert updated_user.password_reset_pending == true
[log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort()
assert ModerationLog.get_log_entry_message(log_entry1) ==
"@#{admin.nickname} updated users: @#{user.nickname}"
assert ModerationLog.get_log_entry_message(log_entry2) ==
"@#{admin.nickname} forced password reset for users: @#{user.nickname}"
end
test "returns 403 if requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{
"password" => "new_password",
"email" => "new_email@example.com",
"name" => "new_name"
})
assert json_response(conn, :forbidden)
end
end
describe "PATCH /users/:nickname/force_password_reset" do
test "sets password_reset_pending to true", %{conn: conn} do
user = insert(:user)
assert user.password_reset_pending == false
conn =
patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
assert json_response(conn, 204) == ""
ObanHelpers.perform_all()
assert User.get_by_id(user.id).password_reset_pending == true
end
end
describe "relays" do
test "POST /relay", %{conn: conn, admin: admin} do
conn =
post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
end
test "GET /relay", %{conn: conn} do
relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
|> Enum.each(fn ap_id ->
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
User.follow(relay_user, user)
end)
conn = get(conn, "/api/pleroma/admin/relay")
assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
end
test "DELETE /relay", %{conn: conn, admin: admin} do
post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
conn =
delete(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
[log_entry_one, log_entry_two] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry_one) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
assert ModerationLog.get_log_entry_message(log_entry_two) ==
"@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
end
end
describe "instances" do
test "GET /instances/:instance/statuses", %{conn: conn} do
user = insert(:user, local: false, nickname: "archaeme@archae.me")
user2 = insert(:user, local: false, nickname: "test@test.com")
insert_pair(:note_activity, user: user)
activity = insert(:note_activity, user: user2)
ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
response = json_response(ret_conn, 200)
assert length(response) == 2
ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses")
response = json_response(ret_conn, 200)
assert length(response) == 1
ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses")
response = json_response(ret_conn, 200)
assert Enum.empty?(response)
CommonAPI.repeat(activity.id, user)
ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
response = json_response(ret_conn, 200)
assert length(response) == 2
ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
response = json_response(ret_conn, 200)
assert length(response) == 3
end
end
describe "PATCH /confirm_email" do
test "it confirms emails of two users", %{conn: conn, admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
ret_conn =
patch(conn, "/api/pleroma/admin/users/confirm_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert ret_conn.status == 200
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
describe "PATCH /resend_confirmation_email" do
test "it resend emails for two users", %{conn: conn, admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
ret_conn =
patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert ret_conn.status == 200
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
describe "POST /reports/:id/notes" do
setup %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting!"
})
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting2!"
})
%{
admin_id: admin.id,
report_id: report_id
}
end
test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
[note, _] = Repo.all(ReportNote)
assert %{
activity_id: ^report_id,
content: "this is disgusting!",
user_id: ^admin_id
} = note
end
test "it returns reports with notes", %{conn: conn, admin: admin} do
conn = get(conn, "/api/pleroma/admin/reports")
response = json_response(conn, 200)
notes = hd(response["reports"])["notes"]
[note, _] = notes
assert note["user"]["nickname"] == admin.nickname
assert note["content"] == "this is disgusting!"
assert note["created_at"]
assert response["total"] == 1
end
test "it deletes the note", %{conn: conn, report_id: report_id} do
assert ReportNote |> Repo.all() |> length() == 2
[note, _] = Repo.all(ReportNote)
delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
assert ReportNote |> Repo.all() |> length() == 1
end
end
test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do
admin = insert(:user, is_admin: true)
conn =
assign(conn, :user, admin)
|> get("/api/pleroma/admin/config/descriptions")
assert [child | _others] = json_response(conn, 200)
assert child["children"]
assert child["key"]
assert String.starts_with?(child["group"], ":")
assert child["description"]
end
describe "/api/pleroma/admin/stats" do
test "status visibility count", %{conn: conn} do
admin = insert(:user, is_admin: true)
user = insert(:user)
CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
CommonAPI.post(user, %{"visibility" => "unlisted", "status" => "hey"})
CommonAPI.post(user, %{"visibility" => "unlisted", "status" => "hey"})
response =
conn
|> assign(:user, admin)
|> get("/api/pleroma/admin/stats")
|> json_response(200)
assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} =
response["status_visibility"]
end
end
describe "POST /api/pleroma/admin/oauth_app" do
test "errors", %{conn: conn} do
response = conn |> post("/api/pleroma/admin/oauth_app", %{}) |> json_response(200)
assert response == %{"name" => "can't be blank", "redirect_uris" => "can't be blank"}
end
test "success", %{conn: conn} do
base_url = Pleroma.Web.base_url()
app_name = "Trusted app"
response =
conn
|> post("/api/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url
})
|> json_response(200)
assert %{
"client_id" => _,
"client_secret" => _,
"name" => ^app_name,
"redirect_uri" => ^base_url,
"trusted" => false
} = response
end
test "with trusted", %{conn: conn} do
base_url = Pleroma.Web.base_url()
app_name = "Trusted app"
response =
conn
|> post("/api/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url,
trusted: true
})
|> json_response(200)
assert %{
"client_id" => _,
"client_secret" => _,
"name" => ^app_name,
"redirect_uri" => ^base_url,
"trusted" => true
} = response
end
end
describe "GET /api/pleroma/admin/oauth_app" do
setup do
app = insert(:oauth_app)
{:ok, app: app}
end
test "list", %{conn: conn} do
response =
conn
|> get("/api/pleroma/admin/oauth_app")
|> json_response(200)
assert %{"apps" => apps, "count" => count, "page_size" => _} = response
assert length(apps) == count
end
test "with page size", %{conn: conn} do
insert(:oauth_app)
page_size = 1
response =
conn
|> get("/api/pleroma/admin/oauth_app", %{page_size: to_string(page_size)})
|> json_response(200)
assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response
assert length(apps) == page_size
end
test "search by client name", %{conn: conn, app: app} do
response =
conn
|> get("/api/pleroma/admin/oauth_app", %{name: app.client_name})
|> json_response(200)
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response
assert returned["client_id"] == app.client_id
assert returned["name"] == app.client_name
end
test "search by client id", %{conn: conn, app: app} do
response =
conn
|> get("/api/pleroma/admin/oauth_app", %{client_id: app.client_id})
|> json_response(200)
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response
assert returned["client_id"] == app.client_id
assert returned["name"] == app.client_name
end
test "only trusted", %{conn: conn} do
app = insert(:oauth_app, trusted: true)
response =
conn
|> get("/api/pleroma/admin/oauth_app", %{trusted: true})
|> json_response(200)
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response
assert returned["client_id"] == app.client_id
assert returned["name"] == app.client_name
end
end
describe "DELETE /api/pleroma/admin/oauth_app/:id" do
test "with id", %{conn: conn} do
app = insert(:oauth_app)
response =
conn
|> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id))
|> json_response(:no_content)
assert response == ""
end
test "with non existance id", %{conn: conn} do
response =
conn
|> delete("/api/pleroma/admin/oauth_app/0")
|> json_response(:bad_request)
assert response == ""
end
end
describe "PATCH /api/pleroma/admin/oauth_app/:id" do
test "with id", %{conn: conn} do
app = insert(:oauth_app)
name = "another name"
url = "https://example.com"
scopes = ["admin"]
id = app.id
website = "http://website.com"
response =
conn
|> patch("/api/pleroma/admin/oauth_app/" <> to_string(app.id), %{
name: name,
trusted: true,
redirect_uris: url,
scopes: scopes,
website: website
})
|> json_response(200)
assert %{
"client_id" => _,
"client_secret" => _,
"id" => ^id,
"name" => ^name,
"redirect_uri" => ^url,
"trusted" => true,
"website" => ^website
} = response
end
test "without id", %{conn: conn} do
response =
conn
|> patch("/api/pleroma/admin/oauth_app/0")
|> json_response(:bad_request)
assert response == ""
end
end
end
# Needed for testing
defmodule Pleroma.Web.Endpoint.NotReal do
end
defmodule Pleroma.Captcha.NotReal do
end
diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
index f86274d57..1ff871c89 100644
--- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
+++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
@@ -1,129 +1,139 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
import Pleroma.Factory
import Ecto.Query
setup do: clear_config([ScheduledActivity, :enabled])
test "shows scheduled activities" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string()
# min_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
# since_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
# max_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
end
test "shows a scheduled activity" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
scheduled_activity = insert(:scheduled_activity, user: user)
res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
+ assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200)
assert scheduled_activity_id == scheduled_activity.id |> to_string()
res_conn = get(conn, "/api/v1/scheduled_statuses/404")
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
test "updates a scheduled activity" do
Pleroma.Config.put([ScheduledActivity, :enabled], true)
%{user: user, conn: conn} = oauth_access(["write:statuses"])
scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60)
{:ok, scheduled_activity} =
ScheduledActivity.create(
user,
%{
scheduled_at: scheduled_at,
params: build(:note).data
}
)
job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities"))
assert job.args == %{"activity_id" => scheduled_activity.id}
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at)
- new_scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 120)
+ new_scheduled_at =
+ NaiveDateTime.utc_now()
+ |> Timex.shift(minutes: 120)
+ |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime)
res_conn =
- put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
scheduled_at: new_scheduled_at
})
- assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
+ assert %{"scheduled_at" => expected_scheduled_at} =
+ json_response_and_validate_schema(res_conn, 200)
+
assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
job = refresh_record(job)
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at)
- res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+ res_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
test "deletes a scheduled activity" do
Pleroma.Config.put([ScheduledActivity, :enabled], true)
%{user: user, conn: conn} = oauth_access(["write:statuses"])
scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60)
{:ok, scheduled_activity} =
ScheduledActivity.create(
user,
%{
scheduled_at: scheduled_at,
params: build(:note).data
}
)
job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities"))
assert job.args == %{"activity_id" => scheduled_activity.id}
res_conn =
conn
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{} = json_response(res_conn, 200)
+ assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Repo.get(ScheduledActivity, scheduled_activity.id)
refute Repo.get(Oban.Job, job.id)
res_conn =
conn
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
end
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index 6791c2fb0..451723e60 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -1,614 +1,620 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Conversation.Participation
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "has an emoji reaction list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 2, me: false},
%{name: "🍵", count: 1, me: false}
]
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 2, me: true},
%{name: "🍵", count: 1, me: false}
]
end
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
[participation] = Participation.for_user(user)
status =
StatusView.render("show.json",
activity: activity,
with_direct_conversation_id: true,
for: user
)
assert status[:pleroma][:direct_conversation_id] == participation.id
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:direct_conversation_id] == nil
end
test "returns the direct conversation id when given the `direct_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
[participation] = Participation.for_user(user)
status =
StatusView.render("show.json",
activity: activity,
direct_conversation_id: participation.id,
for: user
)
assert status[:pleroma][:direct_conversation_id] == participation.id
end
test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
Repo.delete(user)
Cachex.clear(:user_cache)
finger_url =
"https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost"
Tesla.Mock.mock_global(fn
%{method: :get, url: "http://localhost/.well-known/host-meta"} ->
%Tesla.Env{status: 404, body: ""}
%{method: :get, url: "https://localhost/.well-known/host-meta"} ->
%Tesla.Env{status: 404, body: ""}
%{
method: :get,
url: ^finger_url
} ->
%Tesla.Env{status: 404, body: ""}
end)
%{account: ms_user} = StatusView.render("show.json", activity: activity)
assert ms_user.acct == "erroruser@example.com"
end
test "tries to get a user by nickname if fetching by ap_id doesn't work" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
{:ok, user} =
user
|> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
|> Repo.update()
Cachex.clear(:user_cache)
result = StatusView.render("show.json", activity: activity)
assert result[:account][:id] == to_string(user.id)
end
test "a note with null content" do
note = insert(:note_activity)
note_object = Object.normalize(note)
data =
note_object.data
|> Map.put("content", nil)
Object.change(note_object, %{data: data})
|> Object.update_and_set_cache()
User.get_cached_by_ap_id(note.data["actor"])
status = StatusView.render("show.json", %{activity: note})
assert status.content == ""
end
test "a note activity" do
note = insert(:note_activity)
object_data = Object.normalize(note).data
user = User.get_cached_by_ap_id(note.data["actor"])
convo_id = Utils.context_to_conversation_id(object_data["context"])
status = StatusView.render("show.json", %{activity: note})
created_at =
(object_data["published"] || "")
|> String.replace(~r/\.\d+Z/, ".000Z")
expected = %{
id: to_string(note.id),
uri: object_data["id"],
url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
account: AccountView.render("show.json", %{user: user}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
card: nil,
reblog: nil,
content: HTML.filter_tags(object_data["content"]),
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: false,
bookmarked: false,
favourited: false,
muted: false,
pinned: false,
sensitive: false,
poll: nil,
spoiler_text: HTML.filter_tags(object_data["summary"]),
visibility: "public",
media_attachments: [],
mentions: [],
tags: [
%{
name: "#{object_data["tag"]}",
url: "/tag/#{object_data["tag"]}"
}
],
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: [
%{
shortcode: "2hu",
url: "corndog.png",
static_url: "corndog.png",
visible_in_picker: false
}
],
pleroma: %{
local: true,
conversation_id: convo_id,
in_reply_to_account_acct: nil,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
direct_conversation_id: nil,
thread_muted: false,
emoji_reactions: []
}
}
assert status == expected
end
test "tells if the message is muted for some reason" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
opts = %{activity: activity}
status = StatusView.render("show.json", opts)
assert status.muted == false
status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt))
assert status.muted == false
for_opts = %{activity: activity, for: user}
status = StatusView.render("show.json", for_opts)
assert status.muted == true
status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt))
assert status.muted == true
end
test "tells if the message is thread muted" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == false
{:ok, activity} = CommonAPI.add_mute(user, activity)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == true
end
test "tells if the status is bookmarked" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
status = StatusView.render("show.json", %{activity: activity})
assert status.bookmarked == false
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == false
{:ok, _bookmark} = Bookmark.create(user.id, activity.id)
activity = Activity.get_by_id_with_object(activity.id)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == true
end
test "a reply" do
note = insert(:note_activity)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == to_string(note.id)
[status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
assert status.in_reply_to_id == to_string(note.id)
end
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hi @#{mentioned.nickname}"})
status = StatusView.render("show.json", %{activity: activity})
assert status.mentions ==
Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end)
end
test "create mentions from the 'to' field" do
%User{ap_id: recipient_ap_id} = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"to" => [recipient_ap_id],
"cc" => cc
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient_ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient_ap_id
end
test "create mentions from the 'tag' field" do
recipient = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"cc" => cc,
"tag" => [
%{
"href" => recipient.ap_id,
"name" => recipient.nickname,
"type" => "Mention"
},
%{
"href" => "https://example.com/search?tag=test",
"name" => "#test",
"type" => "Hashtag"
}
]
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient.ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient.ap_id
end
test "attachments" do
object = %{
"type" => "Image",
"url" => [
%{
"mediaType" => "image/png",
"href" => "someurl"
}
],
"uuid" => 6
}
expected = %{
id: "1638338801",
type: "image",
url: "someurl",
remote_url: "someurl",
preview_url: "someurl",
text_url: "someurl",
description: nil,
pleroma: %{mime_type: "image/png"}
}
+ api_spec = Pleroma.Web.ApiSpec.spec()
+
assert expected == StatusView.render("attachment.json", %{attachment: object})
+ OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec)
# If theres a "id", use that instead of the generated one
object = Map.put(object, "id", 2)
- assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
+ result = StatusView.render("attachment.json", %{attachment: object})
+
+ assert %{id: "2"} = result
+ OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec)
end
test "put the url advertised in the Activity in to the url attribute" do
id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
[activity] = Activity.search(nil, id)
status = StatusView.render("show.json", %{activity: activity})
assert status.uri == id
assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
end
test "a reblog" do
user = insert(:user)
activity = insert(:note_activity)
{:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
represented = StatusView.render("show.json", %{for: user, activity: reblog})
assert represented[:id] == to_string(reblog.id)
assert represented[:reblog][:id] == to_string(activity.id)
assert represented[:emojis] == []
end
test "a peertube video" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
test "funkwhale audio" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
test "a Mobilizon event" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
end
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
"fediverse",
"mastodon",
"nextcloud",
%{
"href" => "https://kawen.space/users/lain",
"name" => "@lain@kawen.space",
"type" => "Mention"
}
]
assert StatusView.build_tags(object_tags) == [
%{name: "fediverse", url: "/tag/fediverse"},
%{name: "mastodon", url: "/tag/mastodon"},
%{name: "nextcloud", url: "/tag/nextcloud"}
]
end
end
describe "rich media cards" do
test "a rich media card without a site name renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
image: page_url <> "/example.jpg",
title: "Example website"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card without a site name or image renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
title: "Example website"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card without an image renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
site_name: "Example site name",
title: "Example website"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card with all relevant data renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
site_name: "Example site name",
title: "Example website",
image: page_url <> "/example.jpg",
description: "Example description"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
end
test "embeds a relationship in the account" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "drink more water"
})
result = StatusView.render("show.json", %{activity: activity, for: other_user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: other_user, target: user})
end
test "embeds a relationship in the account in reposts" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "˙˙ɐʎns"
})
{:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
result = StatusView.render("show.json", %{activity: activity, for: user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: other_user})
assert result[:reblog][:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: user})
end
test "visibility/list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end
test "successfully renders a Listen activity (pleroma extension)" do
listen_activity = insert(:listen)
status = StatusView.render("listen.json", activity: listen_activity)
assert status.length == listen_activity.data["object"]["length"]
assert status.title == listen_activity.data["object"]["title"]
end
end
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 4b4282727..f4884e0a2 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -1,111 +1,111 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFingerTest do
use Pleroma.DataCase
alias Pleroma.Web.WebFinger
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "host meta" do
test "returns a link to the xml lrdd" do
host_info = WebFinger.host_meta()
assert String.contains?(host_info, Pleroma.Web.base_url())
end
end
describe "incoming webfinger request" do
test "works for fqns" do
user = insert(:user)
{:ok, result} =
WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML")
assert is_binary(result)
end
test "works for ap_ids" do
user = insert(:user)
{:ok, result} = WebFinger.webfinger(user.ap_id, "XML")
assert is_binary(result)
end
end
describe "fingering" do
test "returns error when fails parse xml or json" do
user = "invalid_content@social.heldscal.la"
assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user)
end
test "returns the ActivityPub actor URI for an ActivityPub user" do
user = "framasoft@framatube.org"
{:ok, _data} = WebFinger.finger(user)
end
test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json mimetype" do
user = "kaniini@gerzilla.de"
{:ok, data} = WebFinger.finger(user)
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
end
test "it work for AP-only user" do
user = "kpherox@mstdn.jp"
{:ok, data} = WebFinger.finger(user)
assert data["magic_key"] == nil
assert data["salmon"] == nil
- assert data["topic"] == "https://mstdn.jp/users/kPherox.atom"
+ assert data["topic"] == nil
assert data["subject"] == "acct:kPherox@mstdn.jp"
assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
end
test "it works for friendica" do
user = "lain@squeet.me"
{:ok, _data} = WebFinger.finger(user)
end
test "it gets the xrd endpoint" do
{:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la")
assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}"
end
test "it gets the xrd endpoint for hubzilla" do
{:ok, template} = WebFinger.find_lrdd_template("macgirvin.com")
assert template == "https://macgirvin.com/xrd/?uri={uri}"
end
test "it gets the xrd endpoint for statusnet" do
{:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com")
assert template == "http://status.alpicola.com/main/xrd?uri={uri}"
end
test "it works with idna domains as nickname" do
nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな"))
{:ok, _data} = WebFinger.finger(nickname)
end
test "it works with idna domains as link" do
ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain"
{:ok, _data} = WebFinger.finger(ap_id)
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 12:00 AM (9 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803443
Default Alt Text
(845 KB)

Event Timeline