Page MenuHomePhorge

No OneTemporary

Size
961 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 103b060e1..0ec7e29b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1408 +1,1411 @@
# 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
### Removed
- Non-finch HTTP adapters
-### Upgrade notes
-- Ensure `config :tesla, :adapter` is either unset, or set to `{Tesla.Adapter.Finch, name: MyFinch}` in your .exs config
+- Legacy redirect from /api/pleroma/admin to /api/v1/pleroma/admin
+
### Changed
- Return HTTP error 413 when uploading an avatar or banner that's above the configured upload limit instead of a 500.
+### Upgrade notes
+- Ensure `config :tesla, :adapter` is either unset, or set to `{Tesla.Adapter.Finch, name: MyFinch}` in your .exs config
+
## 2022.12
## Added
- Config: HTTP timeout options, :pool\_timeout and :receive\_timeout
- Added statistic gathering about instances which do/don't have signed fetches when they request from us
- Ability to set a default post expiry time, after which the post will be deleted. If used in concert with ActivityExpiration MRF, the expiry which comes _sooner_ will be applied.
- Regular task to prune local transient activities
- Task to manually run the transient prune job (pleroma.database prune\_task)
- Ability to follow hashtags
- Option to extend `reject` in MRF-Simple to apply to entire threads, where the originating instance is rejected
- Extra information to failed HTTP requests
## Changed
- MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py)
- Relays from akkoma are now off by default
- NormalizeMarkup MRF is now on by default
- Follow/Block/Mute imports now spin off into *n* tasks to avoid the oban timeout
- Transient activities recieved from remote servers are no longer persisted in the database
- Overhauled static-fe view for logged-out users
- Blocked instances will now not be sent _any_ requests, even fetch ones that would get rejected by MRF anyhow
## Removed
- FollowBotPolicy
- Passing of undo/block into MRF
## Upgrade Notes
- If you have an old instance, you will probably want to run `mix pleroma.database prune_task` in the foreground to catch it up with the history of your instance.
## 2022.11
## Added
- Officially supported docker release
- Ability to remove followers unilaterally without a block
- Scraping of nodeinfo from remote instances to display instance info
- `requested_by` in relationships when the user has requested to follow you
## Changed
- Follows no longer override domain blocks, a domain block is final
- Deletes are now the lowest priority to publish and will be handled after creates
- Domain blocks are now subdomain-matches by default
## Fixed
- Registrations via ldap are now compatible with the latest OTP24
## Update notes
- If you use LDAP and run from source, please update your elixir/erlang
to the latest. The changes in OTP24.3 are breaking.
- You can now remove the leading `*.` from domain blocks, but you do not have to.
## 2022.10
### Added
- Ability to sync frontend profiles between clients, with a name attached
- Status card generation will now use the media summary if it is available
### Changed
- Emoji updated to latest 15.0 draft
- **Breaking**: `/api/v1/pleroma/backups` endpoints now requires `read:backups` scope instead of `read:accounts`
- Verify that the signature on posts is not domain blocked, and belongs to the correct user
### Fixed
- OAuthPlug no longer joins with the database every call and uses the user cache
- Undo activities no longer try to look up by ID, and render correctly
- prevent false-errors from meilisearch
## 2022.09
### Added
- support for fedibird-fe, and non-breaking API parity for it to function
- support for setting instance languages in metadata
- support for reusing oauth tokens, and not requiring new authorizations
- the ability to obfuscate domains in your MRF descriptions
- automatic translation of statuses via DeepL or LibreTranslate
- ability to edit posts
- ability to react with remote emoji
### Changed
- MFM parsing is now done on the backend by a modified version of ilja's parser -> https://akkoma.dev/AkkomaGang/mfm-parser
- InlineQuotePolicy is now on by default
- Enable remote users to interact with posts
### Fixed
- Compatibility with latest meilisearch
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
- Elasticsearch returning likes and repeats, displaying as posts
- Ensure key generation happens at registration-time to prevent potential race-conditions
- Ensured websockets get closed on logout
- Allowed GoToSocial-style `?query_string` signatures
### Removed
- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
## 2022.08
### Added
- extended runtime module support, see config cheatsheet
- quote posting; quotes are limited to public posts
### Changed
- quarantining is now considered absolutely; public activities are no longer
an exception.
- also merged quarantine and mrf reject - quarantine is now deprecated
- flavours:
- amd64 is built for debian stable. Compatible with ubuntu 20.
- ubuntu-jammy is built for... well, ubuntu 22 (LTS)
- amd64-musl is built for alpine 3.16
### Fixed
- Updated mastoFE path, for the newer version
### Removed
- Scrobbling support
- `/api/v1/pleroma/scrobble`
- `/api/v1/pleroma/accounts/{id}/scrobbles`
- Deprecated endpoints
- `/api/v1/pleroma/chats`
- `/api/v1/notifications/dismiss`
- `/api/v1/search`
- `/api/v1/statuses/{id}/card`
- Chats, they were half-baked. Just use PMs.
- Prometheus, it causes massive slowdown
## 2022.07
### Added
- Added move account API
- Added ability to set instance accent-color via theme-color
- A fallback page for when a user does not have a frontend installed
- Support for OTP musl11
### Removed
- SSH frontend, to be potentially re-enabled via a bridge rather than wired into the main system
- Gopher frontend, as above
- All pre-compiled javascript
### Fixed
- ES8 support for bulk indexing activities
### Upgrade Notes
- The bundled frontend has been removed, you will need to run the `pleroma.frontend install` mix task to install your frontend of choice. Configuration by default is set to `pleroma-fe`.
- Admin-FE users will have to ensure that :admin is set _BEFORE_ restart, or
you might end up in a situation where you don't have an ability to get it.
## 2.5.2
### Added
- Allow posting and recieving of misskey markdown (requires text/x.misskeymarkdown in `allowed_post_formats`)
### Changed
- Set frontend URLs to akkoma-maintained ones
### Fixed
- Updated `no_empty` MRF to not error when recieving misskey markdown
### Security
- Ensure local-only statuses do not get leaked
## 2.5.1
### Added
- Elasticsearch Search provider support
- Enabled custom emoji reactions to posts via `/api/v1/pleroma/statuses/:id/reactions/:shortcode:`
- Added continuous integration builds for x86 glibc and musl
### Fixed
- Enabled support for non-standard AP entities, such as those used by bookwyrm
## 2.5.0 - 10/06/2022
### Changed
- Allow users to remove their emails if instance does not need email to register
### Added
- `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object
- Experimental support for Finch. Put `config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}` in your secrets file to use it. Reverse Proxy will still use Hackney.
- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis
- AdminAPI: restrict moderators to access sensitive data: change user credentials, get password reset token, read private statuses and chats, etc
- PleromaAPI: Add remote follow API endpoint at `POST /api/v1/pleroma/remote_interaction`
- MastoAPI: Add `GET /api/v1/accounts/lookup`
- MastoAPI: Profile Directory support
- MastoAPI: Support v2 Suggestions (handpicked accounts only)
- Ability to log slow Ecto queries by configuring `:pleroma, :telemetry, :slow_queries_logging`
- Added Phoenix LiveDashboard at `/phoenix/live_dashboard`
- Added `/manifest.json` for progressive web apps.
- Readded mastoFE
- Added support for custom emoji reactions
- Added `emoji_url` in notifications to allow for custom emoji rendering
- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
- Handle Reject for already-accepted Follows properly
- Display OpenGraph data on alternative notice routes.
- Fix replies count for remote replies
- ChatAPI: Add link headers
- Limited number of search results to 40 to prevent DoS attacks
- ActivityPub: fixed federation of attachment dimensions
- Fixed benchmarks
- Elixir 1.13 support
- Fixed crash when pinned_objects is nil
- Fixed slow timelines when there are a lot of deactivated users
- Fixed account deletion API
### Removed
### Security
- Private `/objects/` and `/activities/` leaking if cached by authenticated user
- SweetXML library DTD bomb
## 2.4.2 - 2022-01-10
### Fixed
- Federation issues caused by HTTP pool checkout timeouts
- Compatibility with Elixir 1.13
### Upgrade notes
1. Restart Pleroma
### Changed
- Make `mix pleroma.database set_text_search_config` run concurrently and indefinitely
### Added
- AdminAPI: Missing configuration description for StealEmojiPolicy
### Fixed
- MastodonAPI: Stream out Create activities
- MRF ObjectAgePolicy: Fix pattern matching on "published"
- TwitterAPI: Make `change_password` and `change_email` require params on body instead of query
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
- AdminAPI: Fix rendering reports containing a `nil` object
- Mastodon API: Activity Search fallbacks on status fetching after a DB Timeout/Error
- Mastodon API: Fix crash in Streamer related to reblogging
- AdminAPI: List available frontends when `static/frontends` folder is missing
- Make activity search properly use language-aware GIN indexes
- AdminAPI: Fix suggestions for MRF Policies
## 2.4.0 - 2021-08-08
### Changed
- **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit`
- **Breaking** Entries for simple_policy, transparency_exclusions and quarantined_instances now list both the instance and a reason.
- Support for Erlang/OTP 24
- The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
- HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
- Email address is now returned if requesting user is the owner of the user account so it can be exposed in client and FE user settings UIs.
- Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available.
- AdminAPI: sort users so the newest are at the top.
- ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators
- MRF (`AntiFollowbotPolicy`): Bot accounts are now also considered followbots. Users can still allow bots to follow them by first following the bot.
### Added
- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
- Return OAuth token `id` (primary key) in POST `/oauth/token`.
- AdminAPI: return `created_at` date with users.
- AdminAPI: add DELETE `/api/v1/pleroma/admin/instances/:instance` to delete all content from a remote instance.
- `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time.
- Attachment dimensions and blurhashes are federated when available.
- Mastodon API: support `poll` notification.
- Pinned posts federation
- Possibility to discover users like `user@example.org`, while Akkoma is working on `akkoma.example.org`. Additional configuration required.
### Fixed
- Don't crash so hard when email settings are invalid.
- Checking activated Upload Filters for required commands.
- Remote users can no longer reappear after being deleted.
- Deactivated users may now be deleted.
- Deleting an activity with a lot of likes/boosts no longer causes a database timeout.
- Mix task `pleroma.database prune_objects`
- Fixed rendering of JSON errors on ActivityPub endpoints.
- Linkify: Parsing crash with URLs ending in unbalanced closed paren, no path separator, and no query parameters
- Try to save exported ConfigDB settings (migrate_from_db) in the system temp directory if default location is not writable.
- Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image
- Applying ConcurrentLimiter settings via AdminAPI
- User login failures if their `notification_settings` were in a NULL state.
- Mix task `pleroma.user delete_activities` query transaction timeout is now :infinity
- MRF (`SimplePolicy`): Embedded objects are now checked. If any embedded object would be rejected, its parent is rejected. This fixes Announces leaking posts from blocked domains.
- Fixed some Markdown issues, including trailing slash in links.
### Removed
- **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`)
## [2.3.0] - 2021-03-01
### Security
- Fixed client user agent leaking through MediaProxy
### Removed
- `:auth, :enforce_oauth_admin_scope_usage` configuration option.
### Changed
- **Breaking**: Changed `mix pleroma.user toggle_confirmed` to `mix pleroma.user confirm`
- **Breaking**: Changed `mix pleroma.user toggle_activated` to `mix pleroma.user activate/deactivate`
- **Breaking:** NSFW hashtag is no longer added on sensitive posts
- Polls now always return a `voters_count`, even if they are single-choice.
- Admin Emails: The ap id is used as the user link in emails now.
- Improved registration workflow for email confirmation and account approval modes.
- Search: When using Postgres 11+, Pleroma will use the `websearch_to_tsvector` function to parse search queries.
- Emoji: Support the full Unicode 13.1 set of Emoji for reactions, plus regional indicators.
- Deprecated `Pleroma.Uploaders.S3, :public_endpoint`. Now `Pleroma.Upload, :base_url` is the standard configuration key for all uploaders.
- Improved Apache webserver support: updated sample configuration, MediaProxy cache invalidation verified with the included sample script
- Improve OAuth 2.0 provider support. A missing `fqn` field was added to the response, but does not expose the user's email address.
- Provide redirect of external posts from `/notice/:id` to their original URL
- Admins no longer receive notifications for reports if they are the actor making the report.
- Improved Mailer configuration setting descriptions for AdminFE.
- Updated default avatar to look nicer.
<details>
<summary>API Changes</summary>
- **Breaking:** AdminAPI changed User field `confirmation_pending` to `is_confirmed`
- **Breaking:** AdminAPI changed User field `approval_pending` to `is_approved`
- **Breaking**: AdminAPI changed User field `deactivated` to `is_active`
- **Breaking:** AdminAPI `GET /api/pleroma/admin/users/:nickname_or_id/statuses` changed response format and added the number of total users posts.
- **Breaking:** AdminAPI `GET /api/pleroma/admin/instances/:instance/statuses` changed response format and added the number of total users posts.
- Admin API: Reports now ordered by newest
- Pleroma API: `GET /api/v1/pleroma/chats` is deprecated in favor of `GET /api/v2/pleroma/chats`.
- Pleroma API: Reroute `/api/pleroma/*` to `/api/v1/pleroma/*`
</details>
- Improved hashtag timeline performance (requires a background migration).
### Added
- Reports now generate notifications for admins and mods.
- Support for local-only statuses.
- Support pagination of blocks and mutes.
- Account backup.
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
- `[:activitypub, :blockers_visible]` config to control visibility of blockers.
- Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`.
- The site title is now injected as a `title` tag like preloads or metadata.
- Password reset tokens now are not accepted after a certain age.
- Mix tasks to help with displaying and removing ConfigDB entries. See `mix pleroma.config`.
- OAuth form improvements: users are remembered by their cookie, the CSS is overridable by the admin, and the style has been improved.
- OAuth improvements and fixes: more secure session-based authentication (by token that could be revoked anytime), ability to revoke belonging OAuth token from any client etc.
- Ability to set ActivityPub aliases for follower migration.
- Configurable background job limits for RichMedia (link previews) and MediaProxyWarmingPolicy
- Ability to define custom HTTP headers per each frontend
- MRF (`NoEmptyPolicy`): New MRF Policy which will deny empty statuses or statuses of only mentions from being created by local users
- New users will receive a simple email confirming their registration if no other emails will be dispatched. (e.g., Welcome, Confirmation, or Approval Required)
<details>
<summary>API Changes</summary>
- Admin API: (`GET /api/pleroma/admin/users`) filter users by `unconfirmed` status and `actor_type`.
- Admin API: OpenAPI spec for the user-related operations
- Pleroma API: `GET /api/v2/pleroma/chats` added. It is exactly like `GET /api/v1/pleroma/chats` except supports pagination.
- Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending.
- Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances.
- Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
- Admin API: An endpoint to manage frontends.
- Streaming API: Add follow relationships updates.
- WebPush: Introduce `pleroma:chat_mention` and `pleroma:emoji_reaction` notification types.
- Mastodon API: Add monthly active users to `/api/v1/instance` (`pleroma.stats.mau`).
- Mastodon API: Home, public, hashtag & list timelines accept `only_media`, `remote` & `local` parameters for filtration.
- Mastodon API: `/api/v1/accounts/:id` & `/api/v1/mutes` endpoints accept `with_relationships` parameter and return filled `pleroma.relationship` field.
- Mastodon API: Endpoint to remove a conversation (`DELETE /api/v1/conversations/:id`).
- Mastodon API: `expires_in` in the scheduled post `params` field on `/api/v1/statuses` and `/api/v1/scheduled_statuses/:id` endpoints.
</details>
### Fixed
- Users with `is_discoverable` field set to false (default value) will appear in in-service search results but be hidden from external services (search bots etc.).
- Streaming API: Posts and notifications are not dropped, when CLI task is executing.
- Creating incorrect IPv4 address-style HTTP links when encountering certain numbers.
- Reblog API Endpoint: Do not set visibility parameter to public by default and let CommonAPI to infer it from status, so a user can reblog their private status without explicitly setting reblog visibility to private.
- Tag URLs in statuses are now absolute
- Removed duplicate jobs to purge expired activities
- File extensions of some attachments were incorrectly changed. This feature has been disabled for now.
- Mix task pleroma.instance creates missing parent directories if the configuration or SQL output paths are changed.
<details>
<summary>API Changes</summary>
- Mastodon API: Current user is now included in conversation if it's the only participant.
- Mastodon API: Fixed last_status.account being not filled with account data.
- Mastodon API: Fix not being able to add or remove multiple users at once in lists.
- Mastodon API: Fixed own_votes being not returned with poll data.
- Mastodon API: Fixed creation of scheduled posts with polls.
- Mastodon API: Support for expires_in/expires_at in the Filters.
</details>
## [2.2.2] - 2021-01-18
### Fixed
- StealEmojiPolicy creates dir for emojis, if it doesn't exist.
- Updated `elixir_make` to a non-retired version
### Upgrade notes
1. Restart Pleroma
## [2.2.1] - 2020-12-22
### Changed
- Updated Pleroma FE
### Fixed
- Config generation: rename `Pleroma.Upload.Filter.ExifTool` to `Pleroma.Upload.Filter.Exiftool`.
- S3 Uploads with Elixir 1.11.
- Mix task pleroma.user delete_activities for source installations.
- Search: RUM index search speed has been fixed.
- Rich Media Previews sometimes showed the wrong preview due to a bug following redirects.
- Fixes for the autolinker.
- Forwarded reports duplication from Pleroma instances.
- Emoji Reaction activity filtering from blocked and muted accounts.
- <details>
<summary>API</summary>
- Statuses were not displayed for Mastodon forwarded reports.
</details>
### Upgrade notes
1. Restart Pleroma
## [2.2.0] - 2020-11-12
### Security
- Fixed the possibility of using file uploads to spoof posts.
### Changed
- **Breaking** Requires `libmagic` (or `file`) to guess file types.
- **Breaking:** App metrics endpoint (`/api/pleroma/app_metrics`) is disabled by default, check `docs/API/prometheus.md` on enabling and configuring.
- **Breaking:** Pleroma Admin API: emoji packs and files routes changed.
- **Breaking:** Sensitive/NSFW statuses no longer disable link previews.
- Search: Users are now findable by their urls.
- Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated.
- Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated.
- The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false.
- Users with the `is_discoverable` field set to false will not show up in searches ([bug](https://git.pleroma.social/pleroma/pleroma/-/issues/2301)).
- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option).
- Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`.
- <details>
<summary>API Changes</summary>
- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode.
</details>
### Removed
- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation).
- **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs).
- **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs).
- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were
switched to a new configuration mechanism, however it was not officially removed until now.
### Added
- Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`)
- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email resend_confirmation_emails`)
- Mix task option for force-unfollowing relays
- App metrics: ability to restrict access to specified IP whitelist.
<details>
<summary>API Changes</summary>
- Admin API: Importing emoji from a zip file
- Pleroma API: Importing the mutes users from CSV files.
- Pleroma API: Pagination for remote/local packs and emoji.
</details>
### Fixed
- Add documented-but-missing chat pagination.
- Allow sending out emails again.
- Allow sending chat messages to yourself
- OStatus / static FE endpoints: fixed inaccessibility for anonymous users on non-federating instances, switched to handling per `:restrict_unauthenticated` setting.
- Fix remote users with a whitespace name.
### Upgrade notes
1. Install libmagic and development headers (`libmagic-dev` on Ubuntu/Debian, `file-dev` on Alpine Linux)
2. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
3. Restart Pleroma
## [2.1.2] - 2020-09-17
### Security
- Fix most MRF rules either crashing or not being applied to objects passed into the Common Pipeline (ChatMessage, Question, Answer, Audio, Event).
### Fixed
- Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance.
- Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user.
- Mastodon Streaming API: Handler crashes on authentication failures, resulting in error logs.
- Mastodon Streaming API: Error logs on client pings.
- Rich media: Log spam on failures. Now the error is only logged once per attempt.
### Changed
- Rich Media: A HEAD request is now done to the url, to ensure it has the appropriate content type and size before proceeding with a GET.
### Upgrade notes
1. Restart Pleroma
## [2.1.1] - 2020-09-08
### Security
- Fix possible DoS in Mastodon API user search due to an error in match clauses, leading to an infinite recursion and subsequent OOM with certain inputs.
- Fix metadata leak for accounts and statuses on private instances.
- Fix possible DoS in Admin API search using an atom leak vulnerability. Authentication with admin rights was required to exploit.
### Changed
- **Breaking:** The metadata providers RelMe and Feed are no longer configurable. RelMe should always be activated and Feed only provides a <link> header tag for the actual RSS/Atom feed when the instance is public.
- Improved error message when cmake is not available at build stage.
### Added
- Rich media failure tracking (along with `:failure_backoff` option).
<details>
<summary>Admin API Changes</summary>
- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
</details>
### Fixed
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
- Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers
- Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case.
Reduced to just rich media timeout.
- Mastodon API: Cards being wrong for preview statuses due to cache key collision.
- Password resets no longer processed for deactivated accounts.
- Favicon scraper raising exceptions on URLs longer than 255 characters.
## [2.1.0] - 2020-08-28
### Changed
- **Breaking:** The default descriptions on uploads are now empty. The old behavior (filename as default) can be configured, see the cheat sheet.
- **Breaking:** Added the ObjectAgePolicy to the default set of MRFs. This will delist and strip the follower collection of any message received that is older than 7 days. This will stop users from seeing very old messages in the timelines. The messages can still be viewed on the user's page and in conversations. They also still trigger notifications.
- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
- **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated.
- **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated.
- **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated.
- **Breaking** Changed defaults for `:restrict_unauthenticated` so that when `:instance, :public` is set to `false` then all `:restrict_unauthenticated` items be effectively set to `true`. If you'd like to allow unauthenticated access to specific API endpoints on a private instance, please explicitly set `:restrict_unauthenticated` to non-default value in `config/prod.secret.exs`.
- In Conversations, return only direct messages as `last_status`
- Using the `only_media` filter on timelines will now exclude reblog media
- MFR policy to set global expiration for all local Create activities
- OGP rich media parser merged with TwitterCard
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
<details>
<summary>API Changes</summary>
- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed.
- **Breaking:** Image description length is limited now.
- **Breaking:** Emoji API: changed methods and renamed routes.
- **Breaking:** Notification Settings API for suppressing notifications has been simplified down to `block_from_strangers`.
- **Breaking:** Notification Settings API option for hiding push notification contents has been renamed to `hide_notification_contents`.
- MastodonAPI: Allow removal of avatar, banner and background.
- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream.
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
- Mastodon API: Added `pleroma.metadata.post_formats` to /api/v1/instance
- Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone`
- Mastodon API: Make notifications about statuses from muted users and threads read automatically
- Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value.
</details>
<details>
<summary>Admin API Changes</summary>
- **Breaking** Changed relay `/api/pleroma/admin/relay` endpoints response format.
- Status visibility stats: now can return stats per instance.
- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
</details>
### Removed
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- Frontends: Add mix task to install frontends.
- Frontends: Add configurable frontends for primary and admin fe.
- Configuration: Added a blacklist for email servers.
- Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation.
- Chats: Added support for federated chats. For details, see the docs.
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- Configuration: Add `:database_config_whitelist` setting to whitelist settings which can be configured from AdminFE.
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Mix task to reset MFA for user accounts
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
- Support pagination in emoji packs API (for packs and for files in pack)
- Support for viewing instances favicons next to posts and accounts
- Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
- "By approval" registrations mode.
- Configuration: Added `:welcome` settings for the welcome message to newly registered users. You can send a welcome message as a direct message, chat or email.
- Ability to hide favourites and emoji reactions in the API with `[:instance, :show_reactions]` config.
<details>
<summary>API Changes</summary>
- Mastodon API: Add pleroma.parent_visible field to statuses.
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines.
- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`.
- Mastodon API: Support irreversible property for filters.
- Mastodon API: Add pleroma.favicon field to accounts.
- Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs
</details>
### Fixed
- Fix list pagination and other list issues.
- Support pagination in conversations API
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
- Rich Media Previews for Twitter links
- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
- Fix CSP policy generation to include remote Captcha services
- Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
- Emoji Packs could not be listed when instance was set to `public: false`
- Fix whole_word always returning false on filter get requests
- Migrations not working on OTP releases if the database was connected over ssl
- Fix relay following
## [2.0.7] - 2020-06-13
### Security
- Fix potential DoSes exploiting atom leaks in rich media parser and the `UserAllowListPolicy` MRF policy
### Fixed
- CSP: not allowing images/media from every host when mediaproxy is disabled
- CSP: not adding mediaproxy base url to image/media hosts
- StaticFE missing the CSS file
### Upgrade notes
1. Restart Pleroma
## [2.0.6] - 2020-06-09
### Security
- CSP: harden `image-src` and `media-src` when MediaProxy is used
### Fixed
- AP C2S: Fix pagination in inbox/outbox
- Various compilation errors on OTP 23
- Mastodon API streaming: Repeats from muted threads not being filtered
### Changed
- Various database performance improvements
### Upgrade notes
1. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
2. Restart Pleroma
## [2.0.5] - 2020-05-13
### Security
- Fix possible private status leaks in Mastodon Streaming API
### Fixed
- Crashes when trying to block a user if block federation is disabled
- Not being able to start the instance without `erlang-eldap` installed
- Users with bios over the limit getting rejected
- Follower counters not being updated on incoming follow accepts
### Upgrade notes
1. Restart Pleroma
## [2.0.4] - 2020-05-10
### Security
- AP C2S: Fix a potential DoS by creating nonsensical objects that break timelines
### Fixed
- Peertube user lookups not working
- `InsertSkeletonsForDeletedUsers` migration failing on some instances
- Healthcheck reporting the number of memory currently used, rather than allocated in total
- LDAP not being usable in OTP releases
- Default apache configuration having tls chain issues
### Upgrade notes
#### Apache only
1. Remove the following line from your config:
```
SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem
```
#### Everyone
1. Restart Pleroma
## [2.0.3] - 2020-05-02
### 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
- CSP: Sandbox uploads
### Fixed
- Notifications from blocked domains
- Potential federation issues with Mastodon versions before 3.0.0
- HTTP Basic Authentication permissions issue
- Follow/Block imports not being able to find the user if the nickname started with an `@`
- Instance stats counting internal users
- Inability to run a From Source release without git
- ObjectAgePolicy didn't filter out old messages
- `blob:` urls not being allowed by CSP
### Added
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
- Follow request notifications
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
### Upgrade notes
1. Restart Pleroma
2. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
3. Reset status visibility counters (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl refresh_counter_cache`
- From Source: `mix pleroma.refresh_counter_cache`
## [2.0.2] - 2020-04-08
### 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
- Rate limiter crashes when there is no explicitly specified ip in the config
- 500 errors when no `Accept` header is present if Static-FE is enabled
- 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.
- 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/lib/mix/tasks/pleroma/activity.ex b/lib/mix/tasks/pleroma/activity.ex
index 3a79d8f20..84b9c16f9 100644
--- a/lib/mix/tasks/pleroma/activity.ex
+++ b/lib/mix/tasks/pleroma/activity.ex
@@ -1,57 +1,58 @@
+# credo:disable-for-this-file
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Pagination
require Logger
import Mix.Pleroma
import Ecto.Query
def run(["get", id | _rest]) do
start_pleroma()
id
|> Activity.get_by_id()
|> IO.inspect()
end
def run(["delete_by_keyword", user, keyword | _rest]) do
start_pleroma()
u = User.get_by_nickname(user)
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> Activity.Queries.by_author(u)
|> query_with(keyword)
|> Pagination.fetch_paginated(
%{"offset" => 0, "limit" => 20, "skip_order" => false},
:offset
)
|> Enum.map(fn x -> CommonAPI.delete(x.id, u) end)
|> Enum.count()
|> IO.puts()
end
defp query_with(q, search_query) do
%{rows: [[tsc]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"select current_setting('default_text_search_config')::regconfig::oid;"
)
from([a, o] in q,
where:
fragment(
"to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)",
^tsc,
o.data,
^search_query
)
)
end
end
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index 0881974ee..272c9e3e5 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -1,269 +1,268 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.Conversation
alias Pleroma.Maintenance
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
require Logger
require Pleroma.Constants
import Ecto.Query
import Mix.Pleroma
use Mix.Task
@shortdoc "A collection of database related tasks"
@moduledoc File.read!("docs/docs/administration/CLI_tasks/database.md")
def run(["remove_embedded_objects" | args]) do
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
start_pleroma()
Logger.info("Removing embedded objects")
Repo.query!(
"update activities set data = safe_jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
)
if Keyword.get(options, :vacuum) do
Maintenance.vacuum("full")
end
end
def run(["bump_all_conversations"]) do
start_pleroma()
Conversation.bump_for_all_activities()
end
def run(["update_users_following_followers_counts"]) do
start_pleroma()
Repo.transaction(
fn ->
from(u in User, select: u)
|> Repo.stream()
|> Stream.each(&User.update_follower_count/1)
|> Stream.run()
end,
timeout: :infinity
)
end
def run(["prune_objects" | args]) do
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
start_pleroma()
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])
Logger.info("Pruning objects older than #{deadline} days")
time_deadline =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-(deadline * 86_400))
from(o in Object,
where:
fragment(
"?->'to' \\? ? OR ?->'cc' \\? ?",
o.data,
^Pleroma.Constants.as_public(),
o.data,
^Pleroma.Constants.as_public()
),
where: o.inserted_at < ^time_deadline,
where:
fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host())
)
|> Repo.delete_all(timeout: :infinity)
prune_hashtags_query = """
DELETE FROM hashtags AS ht
WHERE NOT EXISTS (
SELECT 1 FROM hashtags_objects hto
WHERE ht.id = hto.hashtag_id)
"""
Repo.query(prune_hashtags_query)
if Keyword.get(options, :vacuum) do
Maintenance.vacuum("full")
end
end
def run(["prune_task"]) do
start_pleroma()
nil
|> Pleroma.Workers.Cron.PruneDatabaseWorker.perform()
- |> IO.inspect()
end
def run(["fix_likes_collections"]) do
start_pleroma()
from(object in Object,
where: fragment("(?)->>'likes' is not null", object.data),
select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)}
)
|> Pleroma.Repo.chunk_stream(100, :batches)
|> Stream.each(fn objects ->
ids =
objects
|> Enum.filter(fn object -> object.likes |> Jason.decode!() |> is_map() end)
|> Enum.map(& &1.id)
Object
|> where([object], object.id in ^ids)
|> update([object],
set: [
data:
fragment(
"safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
object.data
)
]
)
|> Repo.update_all([], timeout: :infinity)
end)
|> Stream.run()
end
def run(["vacuum", args]) do
start_pleroma()
Maintenance.vacuum(args)
end
def run(["ensure_expiration"]) do
start_pleroma()
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
Pleroma.Activity
|> join(:inner, [a], o in Object,
on:
fragment(
"(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
o.data,
a.data,
a.data
)
)
|> where(local: true)
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> where([_a, o], fragment("?->>'type' = 'Note'", o.data))
|> Pleroma.Repo.chunk_stream(100, :batches)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity ->
expires_at =
activity.inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> Timex.shift(days: days)
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: activity.id,
expires_at: expires_at
})
end)
end)
|> Stream.run()
end
def run(["set_text_search_config", tsconfig]) do
start_pleroma()
%{rows: [[tsc]]} = Ecto.Adapters.SQL.query!(Pleroma.Repo, "SHOW default_text_search_config;")
shell_info("Current default_text_search_config: #{tsc}")
%{rows: [[db]]} = Ecto.Adapters.SQL.query!(Pleroma.Repo, "SELECT current_database();")
shell_info("Update default_text_search_config: #{tsconfig}")
%{messages: msg} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"ALTER DATABASE #{db} SET default_text_search_config = '#{tsconfig}';"
)
# non-exist config will not raise excpetion but only give >0 messages
if length(msg) > 0 do
shell_info("Error: #{inspect(msg, pretty: true)}")
else
rum_enabled = Pleroma.Config.get([:database, :rum_enabled])
shell_info("Recreate index, RUM: #{rum_enabled}")
# Note SQL below needs to be kept up-to-date with latest GIN or RUM index definition in future
if rum_enabled do
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"CREATE OR REPLACE FUNCTION objects_fts_update() RETURNS trigger AS $$ BEGIN
new.fts_content := to_tsvector(new.data->>'content');
RETURN new;
END
$$ LANGUAGE plpgsql",
[],
timeout: :infinity
)
shell_info("Refresh RUM index")
Ecto.Adapters.SQL.query!(Pleroma.Repo, "UPDATE objects SET updated_at = NOW();")
else
Ecto.Adapters.SQL.query!(Pleroma.Repo, "DROP INDEX IF EXISTS objects_fts;")
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"CREATE INDEX CONCURRENTLY objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); ",
[],
timeout: :infinity
)
end
shell_info('Done.')
end
end
# Rolls back a specific migration (leaving subsequent migrations applied).
# WARNING: imposes a risk of unrecoverable data loss — proceed at your own responsibility.
# Based on https://stackoverflow.com/a/53825840
def run(["rollback", version]) do
prompt = "SEVERE WARNING: this operation may result in unrecoverable data loss. Continue?"
if shell_prompt(prompt, "n") in ~w(Yn Y y) do
{_, result, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
version = String.to_integer(version)
re = ~r/^#{version}_.*\.exs/
path = Ecto.Migrator.migrations_path(repo)
with {_, "" <> file} <- {:find, Enum.find(File.ls!(path), &String.match?(&1, re))},
{_, [{mod, _} | _]} <- {:compile, Code.compile_file(Path.join(path, file))},
{_, :ok} <- {:rollback, Ecto.Migrator.down(repo, version, mod)} do
{:ok, "Reversed migration: #{file}"}
else
{:find, _} -> {:error, "No migration found with version prefix: #{version}"}
{:compile, e} -> {:error, "Problem compiling migration module: #{inspect(e)}"}
{:rollback, e} -> {:error, "Problem reversing migration: #{inspect(e)}"}
end
end)
shell_info(inspect(result))
end
end
end
diff --git a/lib/mix/tasks/pleroma/diagnostics.ex b/lib/mix/tasks/pleroma/diagnostics.ex
index b8d8167bb..3914540ca 100644
--- a/lib/mix/tasks/pleroma/diagnostics.ex
+++ b/lib/mix/tasks/pleroma/diagnostics.ex
@@ -1,84 +1,85 @@
+# credo:disable-for-this-file
defmodule Mix.Tasks.Pleroma.Diagnostics do
alias Pleroma.Repo
alias Pleroma.User
require Logger
require Pleroma.Constants
import Mix.Pleroma
import Ecto.Query
use Mix.Task
def run(["http", url]) do
start_pleroma()
Pleroma.HTTP.get(url)
|> IO.inspect()
end
def run(["home_timeline", nickname]) do
start_pleroma()
user = Repo.get_by!(User, nickname: nickname)
Logger.info("Home timeline query #{user.nickname}")
followed_hashtags =
user
|> User.followed_hashtags()
|> Enum.map(& &1.id)
params =
%{limit: 20}
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:followed_hashtags, followed_hashtags)
|> Map.delete(:local)
list_memberships = Pleroma.List.memberships(user)
recipients = [user.ap_id | User.following(user)]
query =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(
recipients ++ list_memberships,
params
)
|> limit(20)
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
|> IO.puts()
end
def run(["user_timeline", nickname, reading_nickname]) do
start_pleroma()
user = Repo.get_by!(User, nickname: nickname)
reading_user = Repo.get_by!(User, nickname: reading_nickname)
Logger.info("User timeline query #{user.nickname}")
params =
%{limit: 20}
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:user, reading_user)
|> Map.put(:actor_id, user.ap_id)
|> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))
list_memberships = Pleroma.List.memberships(user)
recipients =
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> Pleroma.Web.ActivityPub.ActivityPub.user_activities_recipients()
query =
(recipients ++ list_memberships)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(params)
|> limit(20)
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
|> IO.puts()
end
end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 0647c330f..52fd184b5 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -1,324 +1,326 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Instance do
use Mix.Task
import Mix.Pleroma
alias Pleroma.Config
@shortdoc "Manages Pleroma instance"
@moduledoc File.read!("docs/docs/administration/CLI_tasks/instance.md")
def run(["gen" | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
force: :boolean,
output: :string,
output_psql: :string,
domain: :string,
instance_name: :string,
admin_email: :string,
notify_email: :string,
dbhost: :string,
dbname: :string,
dbuser: :string,
dbpass: :string,
rum: :string,
indexable: :string,
db_configurable: :string,
uploads_dir: :string,
static_dir: :string,
listen_ip: :string,
listen_port: :string,
strip_uploads: :string,
anonymize_uploads: :string,
dedupe_uploads: :string
],
aliases: [
o: :output,
f: :force
]
)
paths =
[config_path, psql_path] = [
Keyword.get(options, :output, "config/generated_config.exs"),
Keyword.get(options, :output_psql, "config/setup_db.psql")
]
will_overwrite = Enum.filter(paths, &File.exists?/1)
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
if proceed? do
[domain, port | _] =
String.split(
get_option(
options,
:domain,
"What domain will your instance use? (e.g akkoma.example.com)"
),
":"
) ++ [443]
name =
get_option(
options,
:instance_name,
"What is the name of your instance? (e.g. The Corndog Emporium)",
domain
)
email = get_option(options, :admin_email, "What is your admin email address?")
notify_email =
get_option(
options,
:notify_email,
"What email address do you want to use for sending email notifications?",
email
)
indexable =
get_option(
options,
:indexable,
"Do you want search engines to index your site? (y/n)",
"y"
) === "y"
db_configurable? =
get_option(
options,
:db_configurable,
"Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)",
"n"
) === "y"
dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
dbname = get_option(options, :dbname, "What is the name of your database?", "akkoma")
dbuser =
get_option(
options,
:dbuser,
"What is the user used to connect to your database?",
"akkoma"
)
dbpass =
get_option(
options,
:dbpass,
"What is the password used to connect to your database?",
:crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64),
"autogenerated"
)
rum_enabled =
get_option(
options,
:rum,
"Would you like to use RUM indices?",
"n"
) === "y"
listen_port =
get_option(
options,
:listen_port,
"What port will the app listen to (leave it if you are using the default setup with nginx)?",
4000
)
listen_ip =
get_option(
options,
:listen_ip,
"What ip will the app listen to (leave it if you are using the default setup with nginx)?",
"127.0.0.1"
)
uploads_dir =
get_option(
options,
:uploads_dir,
"What directory should media uploads go in (when using the local uploader)?",
Config.get([Pleroma.Uploaders.Local, :uploads])
)
|> Path.expand()
static_dir =
get_option(
options,
:static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Config.get([:instance, :static_dir])
)
|> Path.expand()
{strip_uploads_message, strip_uploads_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
strip_uploads =
get_option(
options,
:strip_uploads,
strip_uploads_message,
strip_uploads_default
) === "y"
anonymize_uploads =
get_option(
options,
:anonymize_uploads,
"Do you want to anonymize the filenames of uploads? (y/n)",
"n"
) === "y"
dedupe_uploads =
get_option(
options,
:dedupe_uploads,
"Do you want to deduplicate uploaded files? (y/n)",
"n"
) === "y"
Config.put([:instance, :static_dir], static_dir)
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
result_config =
EEx.eval_file(
template_dir <> "/sample_config.eex",
domain: domain,
port: port,
email: email,
notify_email: notify_email,
name: name,
dbhost: dbhost,
dbname: dbname,
dbuser: dbuser,
dbpass: dbpass,
secret: secret,
jwt_secret: jwt_secret,
signing_salt: signing_salt,
lv_signing_salt: lv_signing_salt,
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
db_configurable?: db_configurable?,
static_dir: static_dir,
uploads_dir: uploads_dir,
rum_enabled: rum_enabled,
listen_ip: listen_ip,
listen_port: listen_port,
upload_filters:
upload_filters(%{
strip: strip_uploads,
anonymize: anonymize_uploads,
dedupe: dedupe_uploads
})
)
result_psql =
EEx.eval_file(
template_dir <> "/sample_psql.eex",
dbname: dbname,
dbuser: dbuser,
dbpass: dbpass,
rum_enabled: rum_enabled
)
config_dir = Path.dirname(config_path)
psql_dir = Path.dirname(psql_path)
- [config_dir, psql_dir, static_dir, uploads_dir]
- |> Enum.reject(&File.exists?/1)
- |> Enum.map(&File.mkdir_p!/1)
+ to_create =
+ [config_dir, psql_dir, static_dir, uploads_dir]
+ |> Enum.reject(&File.exists?/1)
+
+ for dir <- to_create do
+ File.mkdir_p!(dir)
+ end
shell_info("Writing config to #{config_path}.")
File.write(config_path, result_config)
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(static_dir, indexable, template_dir)
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
)
if db_configurable? do
shell_info(
" Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
)
end
else
shell_error(
"The task would have overwritten the following files:\n" <>
(Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
)
end
end
defp write_robots_txt(static_dir, indexable, template_dir) do
robots_txt =
EEx.eval_file(
template_dir <> "/robots_txt.eex",
indexable: indexable
)
robots_txt_path = Path.join(static_dir, "robots.txt")
if File.exists?(robots_txt_path) do
File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")
end
File.write(robots_txt_path, robots_txt)
shell_info("Writing #{robots_txt_path}.")
end
defp upload_filters(filters) when is_map(filters) do
enabled_filters =
if filters.strip do
[Pleroma.Upload.Filter.Exiftool]
else
[]
end
enabled_filters =
if filters.anonymize do
enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename]
else
enabled_filters
end
enabled_filters =
if filters.dedupe do
enabled_filters ++ [Pleroma.Upload.Filter.Dedupe]
else
enabled_filters
end
enabled_filters
end
-
- defp upload_filters(_), do: []
end
diff --git a/lib/mix/tasks/pleroma/search.ex b/lib/mix/tasks/pleroma/search.ex
index 67aba79db..102bc5b63 100644
--- a/lib/mix/tasks/pleroma/search.ex
+++ b/lib/mix/tasks/pleroma/search.ex
@@ -1,23 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Search do
use Mix.Task
import Mix.Pleroma
@shortdoc "Manages elasticsearch"
def run(["import", "activities" | _rest]) do
start_pleroma()
- IO.inspect(Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities]))
- IO.inspect(
- Elasticsearch.Index.Bulk.upload(
- Pleroma.Search.Elasticsearch.Cluster,
- "activities",
- Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities])
- )
+ Elasticsearch.Index.Bulk.upload(
+ Pleroma.Search.Elasticsearch.Cluster,
+ "activities",
+ Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities])
)
end
end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index dd1cdca5b..4ca1c28eb 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -1,609 +1,610 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Mix.Pleroma
alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/docs/administration/CLI_tasks/user.md")
def run(["new", nickname, email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
name: :string,
bio: :string,
password: :string,
moderator: :boolean,
admin: :boolean,
assume_yes: :boolean
],
aliases: [
y: :assume_yes
]
)
name = Keyword.get(options, :name, nickname)
bio = Keyword.get(options, :bio, "")
{password, generated_password?} =
case Keyword.get(options, :password) do
nil ->
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
password ->
{password, false}
end
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
assume_yes? = Keyword.get(options, :assume_yes, false)
shell_info("""
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
- password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)}
- name: #{name}
- bio: #{bio}
- moderator: #{if(moderator?, do: "true", else: "false")}
- admin: #{if(admin?, do: "true", else: "false")}
""")
proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
if proceed? do
start_pleroma()
params = %{
nickname: nickname,
email: email,
password: password,
password_confirmation: password,
name: name,
bio: bio
}
changeset = User.register_changeset(%User{}, params, is_confirmed: true)
{:ok, _user} = User.register(changeset)
shell_info("User #{nickname} created")
if moderator? do
run(["set", nickname, "--moderator"])
end
if admin? do
run(["set", nickname, "--admin"])
end
if generated_password? do
run(["reset_password", nickname])
end
else
shell_info("User will not be created.")
end
end
def run(["rm", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.")
else
_ -> shell_error("No local user #{nickname}")
end
end
def run(["reset_password", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
shell_info("Generated password reset token for #{user.nickname}")
IO.puts(
"URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint,
:reset,
token.token)}"
)
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["reset_mfa", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, _token} <- Pleroma.MFA.disable(user) do
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["activate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname),
false <- user.is_active do
User.set_activation(user, true)
:timer.sleep(500)
shell_info("Successfully activated #{nickname}")
else
true ->
shell_info("User #{nickname} already activated")
_ ->
shell_error("No user #{nickname}")
end
end
def run(["deactivate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- user.is_active do
User.set_activation(user, false)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully deactivated #{nickname} and unsubscribed all local followers")
end
else
false ->
shell_info("User #{nickname} already deactivated")
_ ->
shell_error("No user #{nickname}")
end
end
def run(["deactivate_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["deactivate", user.nickname])
end)
end)
|> Stream.run()
end
def run(["set", nickname | rest]) do
start_pleroma()
{options, [], []} =
OptionParser.parse(
rest,
strict: [
admin: :boolean,
confirmed: :boolean,
locked: :boolean,
moderator: :boolean
]
)
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :admin) do
nil -> user
value -> set_admin(user, value)
end
user =
case Keyword.get(options, :confirmed) do
nil -> user
value -> set_confirmation(user, value)
end
user =
case Keyword.get(options, :locked) do
nil -> user
value -> set_locked(user, value)
end
_user =
case Keyword.get(options, :moderator) do
nil -> user
value -> set_moderator(user, value)
end
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["tag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["untag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["refetch_public_keys"]) do
start_pleroma()
Pleroma.User.Query.build(%{
external: true,
is_active: true
})
|> refetch_public_keys()
end
def run(["refetch_public_keys" | rest]) do
start_pleroma()
Pleroma.User.Query.build(%{
ap_id: rest
})
|> refetch_public_keys()
end
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
strict: [
expires_at: :string,
max_use: :integer
]
)
options =
options
|> Keyword.update(:expires_at, {:ok, nil}, fn
nil -> {:ok, nil}
val -> Date.from_iso8601(val)
end)
|> Enum.into(%{})
start_pleroma()
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
invite.token
)
IO.puts(url)
else
error ->
shell_error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
start_pleroma()
shell_info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
expire_info =
with expires_at when not is_nil(expires_at) <- invite.expires_at do
" | Expires at: #{Date.to_string(expires_at)}"
end
using_info =
with max_use when not is_nil(max_use) <- invite.max_use do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}"
)
end)
end
def run(["revoke_invite", token]) do
start_pleroma()
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
shell_info("Invite for token #{token} was revoked.")
else
_ -> shell_error("No invite found with token #{token}")
end
end
def run(["delete_activities", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
shell_info("User #{nickname} statuses deleted.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["change_email", nickname, email]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user
|> User.update_changeset(%{"email" => email})
|> User.update_and_set_cache()
shell_info("#{nickname}'s email updated")
end
end
def run(["show", nickname]) do
start_pleroma()
- nickname
- |> User.get_cached_by_nickname()
- |> IO.inspect()
+ user =
+ nickname
+ |> User.get_cached_by_nickname()
+
+ shell_info("#{inspect(user)}")
end
def run(["send_confirmation", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
- |> IO.inspect()
|> Pleroma.Emails.Mailer.deliver!()
shell_info("#{nickname}'s email sent")
end
end
def run(["confirm", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.confirm(user)
message = if !user.is_confirmed, do: "needs", else: "doesn't need"
shell_info("#{nickname} #{message} confirmation.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["confirm_all"]) do
start_pleroma()
Pleroma.User.Query.build(%{
local: true,
is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user -> User.set_confirmation(user, true) end)
end)
|> Stream.run()
end
def run(["unconfirm_all"]) do
start_pleroma()
Pleroma.User.Query.build(%{
local: true,
is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user -> User.set_confirmation(user, false) end)
end)
|> Stream.run()
end
def run(["sign_out", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.global_sign_out(user)
shell_info("#{nickname} signed out from all apps.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["blocking", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
blocks = User.following_ap_ids(user)
- IO.inspect(blocks, limit: :infinity)
+ IO.puts("#{inspect(blocks)}")
end
end
def run(["timeline_query", nickname]) do
start_pleroma()
params = %{local: true}
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
followed_hashtags =
user
|> User.followed_hashtags()
|> Enum.map(& &1.id)
params =
params
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:limit, 20)
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:local_only, params[:local])
|> Map.put(:hashtags, followed_hashtags)
|> Map.delete(:local)
_activities =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
end
end
def run(["list"]) do
start_pleroma()
Pleroma.User.Query.build(%{local: true})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
shell_info(
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}"
)
end)
end)
|> Stream.run()
end
def run(["fix_follow_state", local_user, remote_user]) do
start_pleroma()
with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
{:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
{:follow_data, %{data: %{"state" => request_state}}} <-
{:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
calculated_state = User.following?(local, remote)
IO.puts(
"Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
)
if calculated_state == false && request_state == "accept" do
IO.puts("Discrepancy found, fixing")
Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
shell_info("Relationship fixed")
else
shell_info("No discrepancy found")
end
else
{:local, _} ->
shell_error("No local user #{local_user}")
{:remote, _} ->
shell_error("No remote user #{remote_user}")
{:follow_data, _} ->
shell_error("No follow data for #{local_user} and #{remote_user}")
end
end
def run(["convert_id", id]) do
{:ok, uuid} = FlakeId.Ecto.Type.dump(id)
{:ok, raw_id} = Ecto.UUID.load(uuid)
shell_info(raw_id)
end
defp refetch_public_keys(query) do
query
|> Pleroma.Repo.chunk_stream(50, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
IO.puts("Re-Resolving: #{user.ap_id}")
with {:ok, user} <- Pleroma.User.fetch_by_ap_id(user.ap_id),
changeset <- Pleroma.User.update_changeset(user),
{:ok, _user} <- Pleroma.User.update_and_set_cache(changeset) do
:ok
else
error -> IO.puts("Could not resolve: #{user.ap_id}, #{inspect(error)}")
end
end)
end)
|> Stream.run()
end
defp set_moderator(user, value) do
{:ok, user} =
user
|> Changeset.change(%{is_moderator: value})
|> User.update_and_set_cache()
shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}")
user
end
defp set_admin(user, value) do
{:ok, user} = User.admin_api_update(user, %{is_admin: value})
shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user
end
defp set_locked(user, value) do
{:ok, user} =
user
|> Changeset.change(%{is_locked: value})
|> User.update_and_set_cache()
shell_info("Locked status of #{user.nickname}: #{user.is_locked}")
user
end
defp set_confirmation(user, value) do
{:ok, user} = User.set_confirmation(user, value)
shell_info("Confirmation status of #{user.nickname}: #{user.is_confirmed}")
user
end
end
diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex
index 30409d93d..e4aaad523 100644
--- a/lib/pleroma/activity/html.ex
+++ b/lib/pleroma/activity/html.ex
@@ -1,81 +1,85 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.HTML do
alias Pleroma.HTML
alias Pleroma.Object
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
# We store a list of cache keys related to an activity in a
# separate cache, scrubber_management_cache. It has the same
# size as scrubber_cache (see application.ex). Every time we add
# a cache to scrubber_cache, we update scrubber_management_cache.
#
# The most recent write of a certain key in the management cache
# is the same as the most recent write of any record related to that
# key in the main cache.
# Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ),
# this means when the management cache is evicted by cachex, all
# related records in the main cache will also have been evicted.
defp get_cache_keys_for(activity_id) do
with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do
list
else
_ -> []
end
end
defp add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id)
unless additional_key in current do
@cachex.put(:scrubber_management_cache, activity_id, [additional_key | current])
end
end
def invalidate_cache_for(activity_id) do
keys = get_cache_keys_for(activity_id)
- Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
+
+ for key <- keys do
+ @cachex.del(:scrubber_cache, key)
+ end
+
@cachex.del(:scrubber_management_cache, activity_id)
end
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
@cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Object.normalize(activity, fetch: false)
add_cache_key_for(activity.id, key)
HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
def get_cached_stripped_html_for_activity(content, activity, key) do
get_cached_scrubbed_html_for_activity(
content,
FastSanitize.Sanitizer.StripTags,
activity,
key,
&HtmlEntities.decode/1
)
end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
defp generate_scrubber_signature(scrubbers) do
Enum.reduce(scrubbers, "", fn scrubber, signature ->
"#{signature}#{to_string(scrubber)}"
end)
end
end
diff --git a/lib/pleroma/akkoma/translators/translator.ex b/lib/pleroma/akkoma/translator.ex
similarity index 100%
rename from lib/pleroma/akkoma/translators/translator.ex
rename to lib/pleroma/akkoma/translator.ex
diff --git a/lib/pleroma/announcement.ex b/lib/pleroma/announcement.ex
index d97c5e728..6dc1a9c7b 100644
--- a/lib/pleroma/announcement.ex
+++ b/lib/pleroma/announcement.ex
@@ -1,160 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Announcement do
use Ecto.Schema
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
import Ecto.Query
alias Pleroma.AnnouncementReadRelationship
alias Pleroma.Repo
@type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "announcements" do
field(:data, :map)
field(:starts_at, :utc_datetime)
field(:ends_at, :utc_datetime)
field(:rendered, :map)
timestamps(type: :utc_datetime)
end
def change(struct, params \\ %{}) do
+ params = validate_params(struct, params)
+
struct
- |> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered])
+ |> cast(params, [:data, :starts_at, :ends_at, :rendered])
|> validate_required([:data])
end
defp validate_params(struct, params) do
base_data =
%{
"content" => "",
"all_day" => false
}
|> Map.merge((struct && struct.data) || %{})
merged_data =
Map.merge(base_data, params.data)
|> Map.take(["content", "all_day"])
params
|> Map.merge(%{data: merged_data})
|> add_rendered_properties()
end
def add_rendered_properties(params) do
{content_html, _, _} =
Pleroma.Web.CommonAPI.Utils.format_input(params.data["content"], "text/plain",
mentions_format: :full
)
rendered = %{
"content" => content_html
}
params
|> Map.put(:rendered, rendered)
end
def add(params) do
changeset = change(%__MODULE__{}, params)
Repo.insert(changeset)
end
def update(announcement, params) do
changeset = change(announcement, params)
Repo.update(changeset)
end
def list_all do
__MODULE__
|> Repo.all()
end
def list_paginated(%{limit: limited_number, offset: offset_number}) do
__MODULE__
|> limit(^limited_number)
|> offset(^offset_number)
|> Repo.all()
end
def get_by_id(id) do
Repo.get_by(__MODULE__, id: id)
end
def delete_by_id(id) do
with announcement when not is_nil(announcement) <- get_by_id(id),
{:ok, _} <- Repo.delete(announcement) do
:ok
else
_ ->
:error
end
end
def read_by?(announcement, user) do
AnnouncementReadRelationship.exists?(user, announcement)
end
def mark_read_by(announcement, user) do
AnnouncementReadRelationship.mark_read(user, announcement)
end
def render_json(announcement, opts \\ []) do
extra_params =
case Keyword.fetch(opts, :for) do
{:ok, user} when not is_nil(user) ->
%{read: read_by?(announcement, user)}
_ ->
%{}
end
admin_extra_params =
case Keyword.fetch(opts, :admin) do
{:ok, true} ->
%{pleroma: %{raw_content: announcement.data["content"]}}
_ ->
%{}
end
base = %{
id: announcement.id,
content: announcement.rendered["content"],
starts_at: announcement.starts_at,
ends_at: announcement.ends_at,
all_day: announcement.data["all_day"],
published_at: announcement.inserted_at,
updated_at: announcement.updated_at,
mentions: [],
statuses: [],
tags: [],
emojis: [],
reactions: []
}
base
|> Map.merge(extra_params)
|> Map.merge(admin_extra_params)
end
# "visible" means:
# starts_at < time < ends_at
def list_all_visible_when(time) do
__MODULE__
|> where([a], is_nil(a.starts_at) or a.starts_at < ^time)
|> where([a], is_nil(a.ends_at) or a.ends_at > ^time)
|> Repo.all()
end
def list_all_visible do
list_all_visible_when(DateTime.now("Etc/UTC") |> elem(1))
end
end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index ec8839e0f..48a2623ce 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,267 +1,270 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do
use Application
import Cachex.Spec
alias Pleroma.Config
require Logger
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
@mix_env Mix.env()
def name, do: @name
def version, do: @version
def named_version, do: @name <> " " <> @version
def repository, do: @repository
def user_agent do
if Process.whereis(Pleroma.Web.Endpoint) do
case Config.get([:http, :user_agent], :default) do
:default ->
info = "#{Pleroma.Web.Endpoint.url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
custom ->
custom
end
else
# fallback, if endpoint is not started yet
"Pleroma Data Loader"
end
end
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
# Scrubbers are compiled at runtime and therefore will cause a conflict
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
# Disable warnings_as_errors at runtime, it breaks Phoenix live reload
# due to protocol consolidation warnings
Code.compiler_options(warnings_as_errors: false)
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
Pleroma.Config.Oban.warn()
Config.DeprecationWarnings.warn()
Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.ApplicationRequirements.verify!()
load_custom_modules()
Pleroma.Docs.JSON.compile()
limiters_setup()
# Define workers and child supervisors to be supervised
children =
[
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
Pleroma.Web.Plugs.RateLimiter.Supervisor,
{Task.Supervisor, name: Pleroma.TaskSupervisor}
] ++
cachex_children() ++
http_children() ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
{Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
elasticsearch_children() ++
task_children(@mix_env) ++
dont_run_in_test(@mix_env)
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
# If we have a lot of caches, default max_restarts can cause test
# resets to fail.
# Go for the default 3 unless we're in test
max_restarts =
if @mix_env == :test do
100
else
3
end
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
with {:ok, data} <- Supervisor.start_link(children, opts) do
set_postgres_server_version()
{:ok, data}
else
e ->
Logger.error("Failed to start!")
Logger.error("#{inspect(e)}")
e
end
end
defp set_postgres_server_version do
version =
with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
{num, _} <- Float.parse(version) do
num
else
e ->
Logger.warn(
"Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
)
9.6
end
:persistent_term.put({Pleroma.Repo, :postgres_version}, version)
end
def load_custom_modules do
dir = Config.get([:modules, :runtime_dir])
if dir && File.exists?(dir) do
dir
|> Pleroma.Utils.compile_dir()
|> case do
{:error, _errors, _warnings} ->
raise "Invalid custom modules"
{:ok, modules, _warnings} ->
if @mix_env != :test do
Enum.each(modules, fn mod ->
Logger.info("Custom module loaded: #{inspect(mod)}")
end)
end
:ok
end
end
end
defp cachex_children do
[
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
build_cachex("user", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("scrubber_management", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000)
]
end
defp emoji_packs_expiration,
do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
@spec build_cachex(String.t(), keyword()) :: map()
def build_cachex(type, opts),
do: %{
id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
type: :worker
}
defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
defp dont_run_in_test(_) do
[
{Registry,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
]}
] ++ background_migrators()
end
defp background_migrators do
[
Pleroma.Migrators.HashtagsTableMigrator
]
end
+ @spec task_children(atom()) :: [map()]
+
defp task_children(:test) do
[
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
}
]
end
defp task_children(_) do
[
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :internal_fetch_init,
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
restart: :temporary
}
]
end
+ @spec elasticsearch_children :: [Pleroma.Search.Elasticsearch.Cluster]
def elasticsearch_children do
config = Config.get([Pleroma.Search, :module])
if config == Pleroma.Search.Elasticsearch do
[Pleroma.Search.Elasticsearch.Cluster]
else
[]
end
end
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
[
Pleroma.Web.RichMedia.Helpers,
Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
Pleroma.Search
]
|> Enum.each(fn module ->
mod_config = Keyword.get(config, module, [])
max_running = Keyword.get(mod_config, :max_running, 5)
max_waiting = Keyword.get(mod_config, :max_waiting, 5)
ConcurrentLimiter.new(module, max_running, max_waiting)
end)
end
defp http_children do
proxy_url = Config.get([:http, :proxy_url])
proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
config =
[:http, :adapter]
|> Config.get([])
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|> Keyword.put(:name, MyFinch)
[{Finch, config}]
end
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index a56311a65..19236aaa2 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -1,237 +1,235 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ApplicationRequirements do
@moduledoc """
The module represents the collection of validations to runs before start server.
"""
defmodule VerifyError, do: defexception([:message])
alias Pleroma.Config
alias Pleroma.Helpers.MediaHelper
import Ecto.Query
require Logger
@spec verify!() :: :ok | VerifyError.t()
def verify! do
:ok
|> check_system_commands!()
|> check_confirmation_accounts!()
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
|> handle_result()
end
defp handle_result(:ok), do: :ok
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do
Logger.warn("""
To send welcome emails, you need to enable the mailer.
Welcome emails will NOT be sent with the current config.
Enable the mailer:
config :pleroma, Pleroma.Emails.Mailer, enabled: true
""")
end
:ok
end
defp check_welcome_message_config!(result), do: result
# Checks account confirmation email
#
def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Emails.Mailer.enabled?() do
Logger.warn("""
Account activation is required, but the mailer is disabled.
Users will NOT be able to confirm their accounts with this config.
Either disable account activation or enable the mailer.
Disable account activation:
config :pleroma, :instance, account_activation_required: false
Enable the mailer:
config :pleroma, Pleroma.Emails.Mailer, enabled: true
""")
end
:ok
end
def check_confirmation_accounts!(result), do: result
# Checks for pending migrations.
#
def check_migrations_applied!(:ok) do
unless Pleroma.Config.get(
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
false
) do
{_, res, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
down_migrations =
Ecto.Migrator.migrations(repo)
|> Enum.reject(fn
{:up, _, _} -> true
{:down, _, _} -> false
end)
if length(down_migrations) > 0 do
down_migrations_text =
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
Logger.error(
"The following migrations were not applied:\n#{down_migrations_text}" <>
"If you want to start Pleroma anyway, set\n" <>
"config :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
)
{:error, "Unapplied Migrations detected"}
else
:ok
end
end)
res
else
:ok
end
end
def check_migrations_applied!(result), do: result
# Checks for settings of RUM indexes.
#
defp check_rum!(:ok) do
{_, res, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
migrate =
from(o in "columns",
where: o.table_name == "objects",
where: o.column_name == "fts_content"
)
|> repo.exists?(prefix: "information_schema")
setting = Pleroma.Config.get([:database, :rum_enabled], false)
do_check_rum!(setting, migrate)
end)
res
end
defp check_rum!(result), do: result
defp do_check_rum!(setting, migrate) do
case {setting, migrate} do
{true, false} ->
Logger.error(
"Use `RUM` index is enabled, but were not applied migrations for it.\n" <>
"If you want to start Pleroma anyway, set\n" <>
"config :pleroma, :database, rum_enabled: false\n" <>
"Otherwise apply the following migrations:\n" <>
"`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
)
{:error, "Unapplied RUM Migrations detected"}
{false, true} ->
Logger.error(
"Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\n" <>
"If you want to use `RUM`, set\n" <>
"config :pleroma, :database, rum_enabled: true\n" <>
"Otherwise roll `RUM` migrations back.\n" <>
"`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
)
{:error, "RUM Migrations detected"}
_ ->
:ok
end
end
defp check_system_commands!(:ok) do
filter_commands_statuses = [
check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "convert"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "ffprobe")
]
preview_proxy_commands_status =
if !Config.get([:media_preview_proxy, :enabled]) or
MediaHelper.missing_dependencies() == [] do
true
else
Logger.error(
"The following dependencies required by Media preview proxy " <>
"(which is currently enabled) are not installed: " <>
inspect(MediaHelper.missing_dependencies())
)
false
end
if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do
:ok
else
{:error,
"System commands missing. Check logs and see `docs/installation` for more details."}
end
end
- defp check_system_commands!(result), do: result
-
defp check_repo_pool_size!(:ok) do
if Pleroma.Config.get([Pleroma.Repo, :pool_size], 10) != 10 and
not Pleroma.Config.get([:dangerzone, :override_repo_pool_size], false) do
Logger.error("""
!!!CONFIG WARNING!!!
The database pool size has been altered from the recommended value of 10.
Please revert or ensure your database is tuned appropriately and then set
`config :pleroma, :dangerzone, override_repo_pool_size: true`.
If you are experiencing database timeouts, please check the "Optimizing
your PostgreSQL performance" section in the documentation. If you still
encounter issues after that, please open an issue on the tracker.
""")
{:error, "Repo.pool_size different than recommended value."}
else
:ok
end
end
defp check_repo_pool_size!(result), do: result
defp check_filter(filter, command_required) do
filters = Config.get([Pleroma.Upload, :filters])
if filter in filters and not Pleroma.Utils.command_available?(command_required) do
Logger.error(
"#{filter} is specified in list of Pleroma.Upload filters, but the " <>
"#{command_required} command is not found"
)
false
else
true
end
end
end
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 8d233d5e4..2ca174c1f 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -1,654 +1,656 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Pack do
@derive {Jason.Encoder, only: [:files, :pack, :files_count]}
defstruct files: %{},
files_count: 0,
pack_file: nil,
path: nil,
pack: %{},
name: nil
@type t() :: %__MODULE__{
files: %{String.t() => Path.t()},
files_count: non_neg_integer(),
pack_file: Path.t(),
path: Path.t(),
pack: map(),
name: String.t()
}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
alias Pleroma.Emoji
alias Pleroma.Emoji.Pack
alias Pleroma.Utils
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do
with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name),
:ok <- File.mkdir(dir) do
save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")})
end
end
defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size)
defp paginate(entities, page, page_size) do
entities
|> Enum.chunk_every(page_size)
|> Enum.at(page - 1)
end
@spec show(keyword()) :: {:ok, t()} | {:error, atom()}
def show(opts) do
name = opts[:name]
with :ok <- validate_not_empty([name]),
{:ok, pack} <- load_pack(name) do
shortcodes =
pack.files
|> Map.keys()
|> Enum.sort()
|> paginate(opts[:page], opts[:page_size])
pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
{:ok, validate_pack(pack)}
end
end
@spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
def delete(name) do
with :ok <- validate_not_empty([name]),
pack_path <- Path.join(emoji_path(), name) do
File.rm_rf(pack_path)
end
end
@spec unpack_zip_emojies(list(tuple())) :: list(map())
defp unpack_zip_emojies(zip_files) do
Enum.reduce(zip_files, [], fn
{_, path, s, _, _, _}, acc when elem(s, 2) == :regular ->
with(
filename <- Path.basename(path),
shortcode <- Path.basename(filename, Path.extname(filename)),
false <- Emoji.exist?(shortcode)
) do
[%{path: path, filename: path, shortcode: shortcode} | acc]
else
_ -> acc
end
_, acc ->
acc
end)
end
@spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) ::
{:ok, t()}
| {:error, File.posix() | atom()}
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
{:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
try do
{:ok, _emoji_files} =
:zip.unzip(
to_charlist(file.path),
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
)
{_, updated_pack} =
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
emoji_file = %Plug.Upload{
filename: item[:filename],
path: Path.join(tmp_dir, item[:path])
}
{:ok, updated_pack} =
do_add_file(
emoji_pack,
item[:shortcode],
to_string(item[:filename]),
emoji_file
)
{item, updated_pack}
end)
Emoji.reload()
{:ok, updated_pack}
after
File.rm_rf(tmp_dir)
end
else
{:error, _} = error ->
error
_ ->
{:ok, pack}
end
end
def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
with :ok <- validate_not_empty([shortcode, filename]),
:ok <- validate_emoji_not_exists(shortcode),
{:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
Emoji.reload()
{:ok, updated_pack}
end
end
defp do_add_file(pack, shortcode, filename, file) do
with :ok <- save_file(file, pack, filename) do
pack
|> put_emoji(shortcode, filename)
|> save_pack()
end
end
@spec delete_file(t(), String.t()) ::
{:ok, t()} | {:error, File.posix() | atom()}
def delete_file(%Pack{} = pack, shortcode) do
with :ok <- validate_not_empty([shortcode]),
:ok <- remove_file(pack, shortcode),
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
Emoji.reload()
{:ok, updated_pack}
end
end
@spec update_file(t(), String.t(), String.t(), String.t(), boolean()) ::
{:ok, t()} | {:error, File.posix() | atom()}
def update_file(%Pack{} = pack, shortcode, new_shortcode, new_filename, force) do
with :ok <- validate_not_empty([shortcode, new_shortcode, new_filename]),
{:ok, filename} <- get_filename(pack, shortcode),
:ok <- validate_emoji_not_exists(new_shortcode, force),
:ok <- rename_file(pack, filename, new_filename),
{:ok, updated_pack} <-
pack
|> delete_emoji(shortcode)
|> put_emoji(new_shortcode, new_filename)
|> save_pack() do
Emoji.reload()
{:ok, updated_pack}
end
end
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
def import_from_filesystem do
emoji_path = emoji_path()
with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
{:ok, results} <- File.ls(emoji_path) do
names =
results
|> Enum.map(&Path.join(emoji_path, &1))
|> Enum.reject(fn path ->
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
end)
|> Enum.map(&write_pack_contents/1)
|> Enum.reject(&is_nil/1)
{:ok, names}
else
{:ok, %{access: _}} -> {:error, :no_read_write}
e -> e
end
end
@spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()}
def list_remote(opts) do
uri = opts[:url] |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri) do
uri
- |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
+ |> URI.merge(
+ "/api/v1/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}"
+ )
|> http_get()
end
end
@spec list_local(keyword()) :: {:ok, map(), non_neg_integer()}
def list_local(opts) do
with {:ok, results} <- list_packs_dir() do
all_packs =
results
|> Enum.map(fn name ->
case load_pack(name) do
{:ok, pack} -> pack
_ -> nil
end
end)
|> Enum.reject(&is_nil/1)
packs =
all_packs
|> paginate(opts[:page], opts[:page_size])
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
{:ok, packs, length(all_packs)}
end
end
@spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
def get_archive(name) do
with {:ok, pack} <- load_pack(name),
:ok <- validate_downloadable(pack) do
{:ok, fetch_archive(pack)}
end
end
@spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()}
def download(name, url, as) do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri),
{:ok, remote_pack} <-
- uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(),
+ uri |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}") |> http_get(),
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
{:ok, archive} <- download_archive(url, sha),
pack <- copy_as(remote_pack, as || name),
{:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pack_info[:fallback] do
save_pack(pack)
else
{:ok, pack}
end
end
end
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
def save_metadata(metadata, %__MODULE__{} = pack) do
pack
|> Map.put(:pack, metadata)
|> save_pack()
end
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
def update_metadata(name, data) do
with {:ok, pack} <- load_pack(name) do
if fallback_sha_changed?(pack, data) do
update_sha_and_save_metadata(pack, data)
else
save_metadata(data, pack)
end
end
end
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"])
with {:ok, _} <- File.stat(pack_file),
{:ok, pack_data} <- File.read(pack_file) do
pack =
from_json(
pack_data,
%{
pack_file: pack_file,
path: Path.dirname(pack_file),
name: name
}
)
files_count =
pack.files
|> Map.keys()
|> length()
{:ok, Map.put(pack, :files_count, files_count)}
end
end
@spec emoji_path() :: Path.t()
defp emoji_path do
[:instance, :static_dir]
|> Pleroma.Config.get!()
|> Path.join("emoji")
end
defp validate_emoji_not_exists(shortcode, force \\ false)
defp validate_emoji_not_exists(_shortcode, true), do: :ok
defp validate_emoji_not_exists(shortcode, _) do
if Emoji.exist?(shortcode) do
{:error, :already_exists}
else
:ok
end
end
defp write_pack_contents(path) do
pack = %__MODULE__{
files: files_from_path(path),
path: path,
pack_file: Path.join(path, "pack.json")
}
case save_pack(pack) do
{:ok, _pack} -> Path.basename(path)
_ -> nil
end
end
defp files_from_path(path) do
txt_path = Path.join(path, "emoji.txt")
if File.exists?(txt_path) do
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
# Make a pack.json file from the contents of that emoji.txt file
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
# Create a map of shortcodes to filenames from emoji.txt
txt_path
|> File.read!()
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(fn line ->
case String.split(line, ~r/,\s*/) do
# This matches both strings with and without tags
# and we don't care about tags here
[name, file | _] ->
file_dir_name = Path.dirname(file)
if String.ends_with?(path, file_dir_name) do
{name, Path.basename(file)}
else
{name, file}
end
_ ->
nil
end
end)
|> Enum.reject(&is_nil/1)
|> Map.new()
else
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions)
end
end
defp validate_pack(pack) do
info =
if downloadable?(pack) do
archive = fetch_archive(pack)
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
pack.pack
|> Map.put("can-download", true)
|> Map.put("download-sha256", archive_sha)
else
Map.put(pack.pack, "can-download", false)
end
Map.put(pack, :pack, info)
end
defp downloadable?(pack) do
# If the pack is set as shared, check if it can be downloaded
# That means that when asked, the pack can be packed and sent to the remote
# Otherwise, they'd have to download it from external-src
pack.pack["share-files"] &&
Enum.all?(pack.files, fn {_, file} ->
pack.path
|> Path.join(file)
|> File.exists?()
end)
end
defp create_archive_and_cache(pack, hash) do
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
{:ok, {_, result}} =
:zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)])
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
@cachex.put(
:emoji_packs_cache,
pack.name,
# if pack.json MD5 changes, the cache is not valid anymore
%{hash: hash, pack_data: result},
# Add a minute to cache time for every file in the pack
ttl: overall_ttl
)
result
end
defp save_pack(pack) do
with {:ok, json} <- Jason.encode(pack, pretty: true),
:ok <- File.write(pack.pack_file, json) do
{:ok, pack}
end
end
defp from_json(json, attrs) do
map = Jason.decode!(json)
pack_attrs =
attrs
|> Map.merge(%{
files: map["files"],
pack: map["pack"]
})
struct(__MODULE__, pack_attrs)
end
defp validate_shareable_packs_available(uri) do
with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
# Get the actual nodeinfo address and fetch it
{:ok, %{"metadata" => %{"features" => features}}} <-
links |> List.last() |> Map.get("href") |> http_get() do
if Enum.member?(features, "shareable_emoji_packs") do
:ok
else
{:error, :not_shareable}
end
end
end
defp validate_not_empty(list) do
if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
:ok
else
{:error, :empty_values}
end
end
defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do
file_path = Path.join(pack.path, filename)
create_subdirs(file_path)
with {:ok, _} <- File.copy(upload_path, file_path) do
:ok
end
end
defp put_emoji(pack, shortcode, filename) do
files = Map.put(pack.files, shortcode, filename)
%{pack | files: files, files_count: length(Map.keys(files))}
end
defp delete_emoji(pack, shortcode) do
files = Map.delete(pack.files, shortcode)
%{pack | files: files}
end
defp rename_file(pack, filename, new_filename) do
old_path = Path.join(pack.path, filename)
new_path = Path.join(pack.path, new_filename)
create_subdirs(new_path)
with :ok <- File.rename(old_path, new_path) do
remove_dir_if_empty(old_path, filename)
end
end
defp create_subdirs(file_path) do
with true <- String.contains?(file_path, "/"),
path <- Path.dirname(file_path),
false <- File.exists?(path) do
File.mkdir_p!(path)
end
end
defp remove_file(pack, shortcode) do
with {:ok, filename} <- get_filename(pack, shortcode),
emoji <- Path.join(pack.path, filename),
:ok <- File.rm(emoji) do
remove_dir_if_empty(emoji, filename)
end
end
defp remove_dir_if_empty(emoji, filename) do
dir = Path.dirname(emoji)
if String.contains?(filename, "/") and File.ls!(dir) == [] do
File.rmdir!(dir)
else
:ok
end
end
defp get_filename(pack, shortcode) do
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
file_path <- Path.join(pack.path, filename),
{:ok, _} <- File.stat(file_path) do
{:ok, filename}
else
{:error, _} = error ->
error
_ ->
{:error, :doesnt_exist}
end
end
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
defp http_get(url) do
with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], []) do
Jason.decode(body)
end
end
defp list_packs_dir do
emoji_path = emoji_path()
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
{:ok, Enum.sort(results)}
else
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
{:ls, {:error, e}} -> {:error, :ls, e}
end
end
defp validate_downloadable(pack) do
if downloadable?(pack), do: :ok, else: {:error, :cant_download}
end
defp copy_as(remote_pack, local_name) do
path = Path.join(emoji_path(), local_name)
%__MODULE__{
name: local_name,
path: path,
files: remote_pack["files"],
pack_file: Path.join(path, "pack.json")
}
end
defp unzip(archive, pack_info, remote_pack, local_pack) do
with :ok <- File.mkdir_p!(local_pack.path) do
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
end
end
defp fetch_pack_info(remote_pack, uri, name) do
case remote_pack["pack"] do
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
{:ok,
%{
sha: sha,
- url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
+ url: URI.merge(uri, "/api/v1/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
{:ok,
%{
sha: sha,
url: src,
fallback: true
}}
_ ->
{:error, "The pack was not set as shared and there is no fallback src to download from"}
end
end
defp download_archive(url, sha) do
with {:ok, %{body: archive}} <- Pleroma.HTTP.get(url) do
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
{:ok, archive}
else
{:error, :invalid_checksum}
end
end
end
defp fetch_archive(pack) do
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
case @cachex.get!(:emoji_packs_cache, pack.name) do
%{hash: ^hash, pack_data: archive} -> archive
_ -> create_archive_and_cache(pack, hash)
end
end
defp fallback_sha_changed?(pack, data) do
is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
end
defp update_sha_and_save_metadata(pack, data) do
with {:ok, %{body: zip}} <- Pleroma.HTTP.get(data[:"fallback-src"]),
:ok <- validate_has_all_files(pack, zip) do
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
data
|> Map.put("fallback-src-sha256", fallback_sha)
|> save_metadata(pack)
end
end
defp validate_has_all_files(pack, zip) do
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
# Check if all files from the pack.json are in the archive
pack.files
|> Enum.all?(fn {_, from_manifest} ->
List.keyfind(f_list, to_charlist(from_manifest), 0)
end)
|> if(do: :ok, else: {:error, :incomplete})
end
end
end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 42db9463d..c489ccbbe 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -1,276 +1,284 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationship do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType
alias Pleroma.FollowingRelationship.State
alias Pleroma.Repo
alias Pleroma.User
+ @type follow_state :: :follow_pending | :follow_accept | :follow_reject | :unfollow
+
schema "following_relationships" do
field(:state, State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
timestamps()
end
@doc "Returns underlying integer code for state atom"
def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
def accept_state_code, do: state_int_code(:follow_accept)
def changeset(%__MODULE__{} = following_relationship, attrs) do
following_relationship
|> cast(attrs, [:state])
|> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following])
|> unique_constraint(:follower_id,
name: :following_relationships_follower_id_following_id_index
)
|> validate_not_self_relationship()
end
def state_to_enum(state) when state in ["pending", "accept", "reject"] do
String.to_existing_atom("follow_#{state}")
end
def state_to_enum(state) do
raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
end
def get(%User{} = follower, %User{} = following) do
__MODULE__
|> where(follower_id: ^follower.id, following_id: ^following.id)
|> Repo.one()
end
def update(follower, following, :follow_reject), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do
nil ->
follow(follower, following, state)
following_relationship ->
with {:ok, _following_relationship} <-
following_relationship
|> cast(%{state: state}, [:state])
|> validate_required([:state])
|> Repo.update() do
after_update(state, follower, following)
end
end
end
+ @spec follow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, any}
def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
with {:ok, _following_relationship} <-
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing) do
after_update(state, follower, following)
end
end
+ @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, any}
def unfollow(%User{} = follower, %User{} = following) do
case get(follower, following) do
%__MODULE__{} = following_relationship ->
with {:ok, _following_relationship} <- Repo.delete(following_relationship) do
after_update(:unfollow, follower, following)
end
_ ->
- {:ok, nil}
+ {:ok, follower, following}
end
end
+ @spec after_update(follow_state(), User.t(), User.t()) ::
+ {:ok, User.t(), User.t()} | {:error, any()}
defp after_update(state, %User{} = follower, %User{} = following) do
with {:ok, following} <- User.update_follower_count(following),
{:ok, follower} <- User.update_following_count(follower) do
Pleroma.Web.Streamer.stream("follow_relationship", %{
state: state,
following: following,
follower: follower
})
{:ok, follower, following}
+ else
+ err -> {:error, err}
end
end
def follower_count(%User{} = user) do
%{followers: user, deactivated: false}
|> User.Query.build()
|> Repo.aggregate(:count, :id)
end
def followers_query(%User{} = user) do
__MODULE__
|> join(:inner, [r], u in User, on: r.follower_id == u.id)
|> where([r], r.following_id == ^user.id)
|> where([r], r.state == ^:follow_accept)
end
def followers_ap_ids(user, from_ap_ids \\ nil)
def followers_ap_ids(_, []), do: []
def followers_ap_ids(%User{} = user, from_ap_ids) do
query =
user
|> followers_query()
|> select([r, u], u.ap_id)
query =
if from_ap_ids do
where(query, [r, u], u.ap_id in ^from_ap_ids)
else
query
end
Repo.all(query)
end
def following_count(%User{id: nil}), do: 0
def following_count(%User{} = user) do
%{friends: user, deactivated: false}
|> User.Query.build()
|> Repo.aggregate(:count, :id)
end
def get_follow_requests(%User{id: id}) do
__MODULE__
|> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id)
|> where([r, f], f.is_active == true)
|> select([r, f], f)
|> Repo.all()
end
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
|> Repo.exists?()
end
def following_query(%User{} = user) do
__MODULE__
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|> where([r], r.follower_id == ^user.id)
|> where([r], r.state == ^:follow_accept)
end
def outgoing_pending_follow_requests_query(%User{} = follower) do
__MODULE__
|> where([r], r.follower_id == ^follower.id)
|> where([r], r.state == ^:follow_pending)
end
def following(%User{} = user) do
following =
following_query(user)
|> select([r, u], u.follower_address)
|> Repo.all()
if not user.local or user.invisible do
following
else
[user.follower_address | following]
end
end
def move_following(origin, target) do
__MODULE__
|> join(:inner, [r], f in assoc(r, :follower))
|> where(following_id: ^origin.id)
|> where([r, f], f.allow_following_move == true)
|> where([r, f], f.local == true)
|> limit(50)
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
end)
|> case do
[] ->
User.update_follower_count(origin)
:ok
_ ->
move_following(origin, target)
end
end
def all_between_user_sets(
source_users,
target_users
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
__MODULE__
|> where(
fragment(
"(follower_id = ANY(?) AND following_id = ANY(?)) OR \
(follower_id = ANY(?) AND following_id = ANY(?))",
^source_user_ids,
^target_user_ids,
^target_user_ids,
^source_user_ids
)
)
|> Repo.all()
end
def find(following_relationships, follower, following) do
Enum.find(following_relationships, fn
fr -> fr.follower_id == follower.id and fr.following_id == following.id
end)
end
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
|> validate_following_id_follower_id_inequality()
end
defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :follower_id, fn _, follower_id ->
if follower_id == get_field(changeset, :following_id) do
[source_id: "can't be equal to following_id"]
else
[]
end
end)
end
defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :following_id, fn _, following_id ->
if following_id == get_field(changeset, :follower_id) do
[target_id: "can't be equal to follower_id"]
else
[]
end
end)
end
@spec following_ap_ids(User.t()) :: [String.t()]
def following_ap_ids(%User{} = user) do
user
|> following_query()
|> select([r, u], u.ap_id)
|> Repo.all()
end
end
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
index d0c3ab5cc..cb95d0e68 100644
--- a/lib/pleroma/helpers/media_helper.ex
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -1,162 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.MediaHelper do
@moduledoc """
Handles common media-related operations.
"""
alias Pleroma.HTTP
require Logger
def missing_dependencies do
Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
if Pleroma.Utils.command_available?(executable) do
acc
else
[sym | acc]
end
end)
end
def image_resize(url, options) do
with executable when is_binary(executable) <- System.find_executable("convert"),
{:ok, args} <- prepare_image_resize_args(options),
{:ok, env} <- HTTP.get(url, [], []),
{:ok, fifo_path} <- mkfifo() do
args = List.flatten([fifo_path, args])
run_fifo(fifo_path, env, executable, args)
else
nil -> {:error, {:convert, :command_not_found}}
{:error, _} = error -> error
end
end
defp prepare_image_resize_args(
%{max_width: max_width, max_height: max_height, format: "png"} = options
) do
quality = options[:quality] || 85
resize = Enum.join([max_width, "x", max_height, ">"])
args = [
"-resize",
resize,
"-quality",
to_string(quality),
"png:-"
]
{:ok, args}
end
defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
quality = options[:quality] || 85
resize = Enum.join([max_width, "x", max_height, ">"])
args = [
"-interlace",
"Plane",
"-resize",
resize,
"-quality",
to_string(quality),
"jpg:-"
]
{:ok, args}
end
defp prepare_image_resize_args(_), do: {:error, :missing_options}
# Note: video thumbnail is intentionally not resized (always has original dimensions)
def video_framegrab(url) do
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
{:ok, env} <- HTTP.get(url, [], []),
{:ok, fifo_path} <- mkfifo(),
args = [
"-y",
"-i",
fifo_path,
"-vframes",
"1",
"-f",
"mjpeg",
"-loglevel",
"error",
"-"
] do
run_fifo(fifo_path, env, executable, args)
else
nil -> {:error, {:ffmpeg, :command_not_found}}
{:error, _} = error -> error
end
end
defp run_fifo(fifo_path, env, executable, args) do
pid =
Port.open({:spawn_executable, executable}, [
:use_stdio,
:stream,
:exit_status,
:binary,
args: args
])
- fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
+ fifo = File.open!(fifo_path, [:append, :binary])
fix = Pleroma.Helpers.QtFastStart.fix(env.body)
- true = Port.command(fifo, fix)
- :erlang.port_close(fifo)
+ IO.binwrite(fifo, fix)
+ File.close(fifo)
loop_recv(pid)
after
File.rm(fifo_path)
end
defp mkfifo do
path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
case System.cmd("mkfifo", [path]) do
{_, 0} ->
spawn(fifo_guard(path))
{:ok, path}
{_, err} ->
{:error, {:fifo_failed, err}}
end
end
defp fifo_guard(path) do
pid = self()
fn ->
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, _} ->
File.rm(path)
end
end
end
defp loop_recv(pid) do
loop_recv(pid, <<>>)
end
defp loop_recv(pid, acc) do
receive do
{^pid, {:data, data}} ->
loop_recv(pid, acc <> data)
{^pid, {:exit_status, 0}} ->
{:ok, acc}
{^pid, {:exit_status, status}} ->
{:error, status}
after
5000 ->
:erlang.port_close(pid)
{:error, :timeout}
end
end
end
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
index e837ac8d4..77b854b5d 100644
--- a/lib/pleroma/http/adapter_helper.ex
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -1,150 +1,146 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper do
@moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
@defaults [name: MyFinch, pool_timeout: 5_000, receive_timeout: 5_000]
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | :inet.ip_address()
alias Pleroma.HTTP.AdapterHelper
require Logger
- @type proxy ::
- {Connection.host(), pos_integer()}
- | {Connection.proxy_type(), Connection.host(), pos_integer()}
+ @type proxy :: {Connection.proxy_type(), Connection.host(), pos_integer(), list()}
@callback options(keyword(), URI.t()) :: keyword()
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
def format_proxy(nil), do: nil
def format_proxy(proxy_url) do
case parse_proxy(proxy_url) do
- {:ok, host, port} -> {:http, host, port, []}
{:ok, type, host, port} -> {type, host, port, []}
_ -> nil
end
end
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
def maybe_add_proxy(opts, nil), do: opts
def maybe_add_proxy(opts, proxy) do
Keyword.put(opts, :proxy, proxy)
end
def maybe_add_proxy_pool(opts, nil), do: opts
def maybe_add_proxy_pool(opts, proxy) do
Logger.info("Using HTTP Proxy: #{inspect(proxy)}")
opts
|> maybe_add_pools()
|> maybe_add_default_pool()
|> maybe_add_conn_opts()
|> put_in([:pools, :default, :conn_opts, :proxy], proxy)
end
defp maybe_add_pools(opts) do
if Keyword.has_key?(opts, :pools) do
opts
else
Keyword.put(opts, :pools, %{})
end
end
defp maybe_add_default_pool(opts) do
pools = Keyword.get(opts, :pools)
if Map.has_key?(pools, :default) do
opts
else
put_in(opts, [:pools, :default], [])
end
end
defp maybe_add_conn_opts(opts) do
conn_opts = get_in(opts, [:pools, :default, :conn_opts])
unless is_nil(conn_opts) do
opts
else
put_in(opts, [:pools, :default, :conn_opts], [])
end
end
@doc """
Merge default connection & adapter options with received ones.
"""
@spec options(URI.t(), keyword()) :: keyword()
def options(%URI{} = uri, opts \\ []) do
@defaults
|> Keyword.merge(opts)
|> AdapterHelper.Default.options(uri)
end
defp proxy_type("http"), do: {:ok, :http}
defp proxy_type("https"), do: {:ok, :https}
defp proxy_type(_), do: {:error, :unknown}
@spec parse_proxy(String.t() | tuple() | nil) ::
- {:ok, host(), pos_integer()}
- | {:ok, proxy_type(), host(), pos_integer()}
+ {:ok, proxy_type(), host(), pos_integer()}
| {:error, atom()}
| nil
def parse_proxy(nil), do: nil
def parse_proxy(""), do: nil
def parse_proxy(proxy) when is_binary(proxy) do
with %URI{} = uri <- URI.parse(proxy),
{:ok, type} <- proxy_type(uri.scheme) do
{:ok, type, uri.host, uri.port}
else
e ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}, #{inspect(e)}")
{:error, :invalid_proxy}
end
end
def parse_proxy(proxy) when is_tuple(proxy) do
with {type, host, port} <- proxy do
{:ok, type, host, port}
else
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address()
def parse_host(host) when is_list(host), do: host
def parse_host(host) when is_atom(host), do: to_charlist(host)
def parse_host(host) when is_binary(host) do
host = to_charlist(host)
case :inet.parse_address(host) do
{:error, :einval} -> host
{:ok, ip} -> ip
end
end
@spec format_host(String.t()) :: charlist()
def format_host(host) do
host_charlist = to_charlist(host)
case :inet.parse_address(host_charlist) do
{:error, :einval} ->
:idna.encode(host_charlist)
{:ok, _ip} ->
host_charlist
end
end
end
diff --git a/lib/pleroma/migrators/support/base_migrator.ex b/lib/pleroma/migrators/support/base_migrator.ex
index 1f8a5402b..2ffb35081 100644
--- a/lib/pleroma/migrators/support/base_migrator.ex
+++ b/lib/pleroma/migrators/support/base_migrator.ex
@@ -1,210 +1,210 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Migrators.Support.BaseMigrator do
@moduledoc """
Base background migrator functionality.
"""
@callback perform() :: any()
@callback retry_failed() :: any()
@callback feature_config_path() :: list(atom())
@callback query() :: Ecto.Query.t()
@callback fault_rate_allowance() :: integer() | float()
defmacro __using__(_opts) do
- quote do
+ quote generated: true do
use GenServer
require Logger
import Ecto.Query
alias __MODULE__.State
alias Pleroma.Config
alias Pleroma.Repo
@behaviour Pleroma.Migrators.Support.BaseMigrator
defdelegate data_migration(), to: State
defdelegate data_migration_id(), to: State
defdelegate state(), to: State
defdelegate persist_state(), to: State, as: :persist_to_db
defdelegate get_stat(key, value \\ nil), to: State, as: :get_data_key
defdelegate put_stat(key, value), to: State, as: :put_data_key
defdelegate increment_stat(key, increment), to: State, as: :increment_data_key
@reg_name {:global, __MODULE__}
def whereis, do: GenServer.whereis(@reg_name)
def start_link(_) do
case whereis() do
nil ->
GenServer.start_link(__MODULE__, nil, name: @reg_name)
pid ->
{:ok, pid}
end
end
@impl true
def init(_) do
{:ok, nil, {:continue, :init_state}}
end
@impl true
def handle_continue(:init_state, _state) do
{:ok, _} = State.start_link(nil)
data_migration = data_migration()
manual_migrations = Config.get([:instance, :manual_data_migrations], [])
cond do
Config.get(:env) == :test ->
update_status(:noop)
is_nil(data_migration) ->
message = "Data migration does not exist."
update_status(:failed, message)
Logger.error("#{__MODULE__}: #{message}")
data_migration.state == :manual or data_migration.name in manual_migrations ->
message = "Data migration is in manual execution or manual fix mode."
update_status(:manual, message)
Logger.warn("#{__MODULE__}: #{message}")
data_migration.state == :complete ->
on_complete(data_migration)
true ->
send(self(), :perform)
end
{:noreply, nil}
end
@impl true
def handle_info(:perform, state) do
State.reinit()
update_status(:running)
put_stat(:iteration_processed_count, 0)
put_stat(:started_at, NaiveDateTime.utc_now())
perform()
fault_rate = fault_rate()
put_stat(:fault_rate, fault_rate)
fault_rate_allowance = fault_rate_allowance()
cond do
fault_rate == 0 ->
set_complete()
is_float(fault_rate) and fault_rate <= fault_rate_allowance ->
message = """
Done with fault rate of #{fault_rate} which doesn't exceed #{fault_rate_allowance}.
Putting data migration to manual fix mode. Try running `#{__MODULE__}.retry_failed/0`.
"""
Logger.warn("#{__MODULE__}: #{message}")
update_status(:manual, message)
on_complete(data_migration())
true ->
message = "Too many failures. Try running `#{__MODULE__}.retry_failed/0`."
Logger.error("#{__MODULE__}: #{message}")
update_status(:failed, message)
end
persist_state()
{:noreply, state}
end
defp on_complete(data_migration) do
if data_migration.feature_lock || feature_state() == :disabled do
Logger.warn(
"#{__MODULE__}: migration complete but feature is locked; consider enabling."
)
:noop
else
Config.put(feature_config_path(), :enabled)
:ok
end
end
@doc "Approximate count for current iteration (including processed records count)"
def count(force \\ false, timeout \\ :infinity) do
stored_count = get_stat(:count)
if stored_count && !force do
stored_count
else
processed_count = get_stat(:processed_count, 0)
max_processed_id = get_stat(:max_processed_id, 0)
query = where(query(), [entity], entity.id > ^max_processed_id)
count = Repo.aggregate(query, :count, :id, timeout: timeout) + processed_count
put_stat(:count, count)
persist_state()
count
end
end
def failures_count do
with {:ok, %{rows: [[count]]}} <-
Repo.query(
"SELECT COUNT(record_id) FROM data_migration_failed_ids WHERE data_migration_id = $1;",
[data_migration_id()]
) do
count
end
end
def feature_state, do: Config.get(feature_config_path())
def force_continue do
send(whereis(), :perform)
end
def force_restart do
:ok = State.reset()
force_continue()
end
def set_complete do
update_status(:complete)
persist_state()
on_complete(data_migration())
end
defp update_status(status, message \\ nil) do
put_stat(:state, status)
put_stat(:message, message)
end
defp fault_rate do
with failures_count when is_integer(failures_count) <- failures_count() do
failures_count / Enum.max([get_stat(:affected_count, 0), 1])
else
_ -> :error
end
end
defp records_per_second do
get_stat(:iteration_processed_count, 0) / Enum.max([running_time(), 1])
end
defp running_time do
NaiveDateTime.diff(
NaiveDateTime.utc_now(),
get_stat(:started_at, NaiveDateTime.utc_now())
)
end
end
end
end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 7da8d0c63..b94d53913 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -1,579 +1,580 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ModerationLog do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
@type t :: %__MODULE__{}
@type log_subject :: Activity.t() | User.t() | list(User.t())
@type log_params :: %{
required(:actor) => User.t(),
required(:action) => String.t(),
optional(:subject) => log_subject(),
optional(:subject_actor) => User.t(),
optional(:subject_id) => String.t(),
optional(:subjects) => list(User.t()),
optional(:permission) => String.t(),
optional(:text) => String.t(),
optional(:sensitive) => String.t(),
optional(:visibility) => String.t(),
optional(:followed) => User.t(),
optional(:follower) => User.t(),
optional(:nicknames) => list(String.t()),
optional(:tags) => list(String.t()),
optional(:target) => String.t()
}
schema "moderation_log" do
field(:data, :map)
timestamps()
end
def get_all(params) do
base_query =
get_all_query()
|> maybe_filter_by_date(params)
|> maybe_filter_by_user(params)
|> maybe_filter_by_search(params)
query_with_pagination = base_query |> paginate_query(params)
%{
items: Repo.all(query_with_pagination),
count: Repo.aggregate(base_query, :count, :id)
}
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date)
)
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
from(q in query,
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date),
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_user(query, %{user_id: nil}), do: query
defp maybe_filter_by_user(query, %{user_id: user_id}) do
from(q in query,
where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
)
end
defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
do: query
defp maybe_filter_by_search(query, %{search: search}) do
from(q in query,
where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
)
end
defp paginate_query(query, %{page: page, page_size: page_size}) do
from(q in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
defp get_all_query do
from(q in __MODULE__,
order_by: [desc: q.inserted_at]
)
end
defp parse_datetime(datetime) do
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
parsed_datetime
end
defp prepare_log_data(%{actor: actor, action: action} = attrs) do
%{
"actor" => user_to_map(actor),
"action" => action,
"message" => ""
}
|> Pleroma.Maps.put_if_present("subject_actor", user_to_map(attrs[:subject_actor]))
end
defp prepare_log_data(attrs), do: attrs
@spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject" => user_to_map(subjects), "permission" => permission})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
when action in ["report_note_delete", "report_update", "report_note"] do
data =
attrs
|> prepare_log_data
|> Pleroma.Maps.put_if_present("text", attrs[:text])
|> Map.merge(%{"subject" => report_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(
%{
actor: %User{},
action: action,
subject: %Activity{} = subject,
sensitive: sensitive,
visibility: visibility
} = attrs
)
when action == "status_update" do
data =
attrs
|> prepare_log_data
|> Map.merge(%{
"subject" => status_to_map(subject),
"sensitive" => sensitive,
"visibility" => visibility
})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(%{actor: %User{}, action: action, subject_id: subject_id} = attrs)
when action == "status_delete" do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject_id" => subject_id})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(%{actor: %User{}, subject: subject, action: _action} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject" => user_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(%{actor: %User{}, subjects: subjects, action: _action} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subjects" => user_to_map(subjects)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(
%{
actor: %User{},
followed: %User{} = followed,
follower: %User{} = follower,
action: action
} = attrs
)
when action in ["unfollow", "follow"] do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"followed" => user_to_map(followed), "follower" => user_to_map(follower)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
tags: tags,
action: action
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"nicknames" => nicknames,
"tags" => tags,
"action" => action,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
def insert_log(%{actor: %User{}, action: action, target: target} = attrs)
when action in ["relay_follow", "relay_unfollow"] do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"target" => target})
insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
+ @spec insert_log_entry_with_message(ModerationLog.t()) ::
+ {:ok, ModerationLog.t()} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
|> Repo.insert()
end
defp user_to_map(users) when is_list(users) do
Enum.map(users, &user_to_map/1)
end
defp user_to_map(%User{} = user) do
user
|> Map.take([:id, :nickname])
|> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end
defp user_to_map(_), do: nil
defp report_to_map(%Activity{} = report) do
%{"type" => "report", "id" => report.id, "state" => report.data["state"]}
end
defp status_to_map(%Activity{} = status) do
%{"type" => "status", "id" => status.id}
end
@spec get_log_entry_message(ModerationLog.t()) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => action,
"followed" => %{"nickname" => followed_nickname},
"follower" => %{"nickname" => follower_nickname}
}
}) do
"@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "delete",
"subject" => subjects
}
}) do
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "create",
"subjects" => subjects
}
}) do
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
"subject" => users
}
}) do
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
"subject" => users
}
}) do
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "approve",
"subject" => users
}
}) do
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "add_suggestion",
"subject" => users
}
}) do
"@#{actor_nickname} added suggested users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "remove_suggestion",
"subject" => users
}
}) do
"@#{actor_nickname} removed suggested users: #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"nicknames" => nicknames,
"tags" => tags,
"action" => "tag"
}
}) do
tags_string = tags |> Enum.join(", ")
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"nicknames" => nicknames,
"tags" => tags,
"action" => "untag"
}
}) do
tags_string = tags |> Enum.join(", ")
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
"subject" => users,
"permission" => permission
}
}) do
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
"subject" => users,
"permission" => permission
}
}) do
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "relay_follow",
"target" => target
}
}) do
"@#{actor_nickname} followed relay: #{target}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "relay_unfollow",
"target" => target
}
}) do
"@#{actor_nickname} unfollowed relay: #{target}"
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_update",
"subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
}
} = log
) do
"@#{actor_nickname} updated report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" with '#{state}' state"
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
} = log
) do
"@#{actor_nickname} added note '#{text}' to report ##{subject_id}" <>
subject_actor_nickname(log, " on user ")
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note_delete",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
} = log
) do
"@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" <>
subject_actor_nickname(log, " on user ")
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => nil,
"visibility" => visibility
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => sensitive,
"visibility" => nil
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => sensitive,
"visibility" => visibility
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{visibility}'"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_delete",
"subject_id" => subject_id
}
}) do
"@#{actor_nickname} deleted status ##{subject_id}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "force_password_reset",
"subject" => subjects
}
}) do
"@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "confirm_email",
"subject" => subjects
}
}) do
"@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "resend_confirmation_email",
"subject" => subjects
}
}) do
"@#{actor_nickname} re-sent confirmation email for users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "updated_users",
"subject" => subjects
}
}) do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "create_backup",
"subject" => %{"nickname" => user_nickname}
}
}) do
"@#{actor_nickname} requested account backup for @#{user_nickname}"
end
defp nicknames_to_string(nicknames) do
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
end
defp users_to_nicknames_string(users) do
users
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
end
defp subject_actor_nickname(%ModerationLog{data: data}, prefix_msg, postfix_msg \\ "") do
case data do
%{"subject_actor" => %{"nickname" => subject_actor}} ->
[prefix_msg, "@#{subject_actor}", postfix_msg]
|> Enum.reject(&(&1 == ""))
|> Enum.join()
_ ->
""
end
end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index a75d85c47..844251a18 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,429 +1,429 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Workers.AttachmentsCleanupWorker
require Logger
@type t() :: %__MODULE__{}
@derive {Jason.Encoder, only: [:data]}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "objects" do
field(:data, :map)
many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
timestamps()
end
def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
object_position = Map.get(query.aliases, :object, 0)
join(query, join_type, [{object, object_position}], a in Activity,
on:
fragment(
"COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
a.data,
object.data,
a.data,
^activity_type
),
as: :object_activity
)
end
def create(data) do
%Object{}
|> Object.change(%{data: data})
|> Repo.insert()
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
# Expecting `maybe_handle_hashtags_change/1` to run last:
|> maybe_handle_hashtags_change(struct)
end
# Note: not checking activity type (assuming non-legacy objects are associated with Create act.)
defp maybe_handle_hashtags_change(changeset, struct) do
with %Ecto.Changeset{valid?: true} <- changeset,
data_hashtags_change = get_change(changeset, :data),
{_, true} <- {:changed, hashtags_changed?(struct, data_hashtags_change)},
{:ok, hashtag_records} <-
data_hashtags_change
|> object_data_hashtags()
|> Hashtag.get_or_create_by_names() do
put_assoc(changeset, :hashtags, hashtag_records)
else
%{valid?: false} ->
changeset
{:changed, false} ->
changeset
{:error, _} ->
validate_change(changeset, :data, fn _, _ ->
[data: "error referencing hashtags"]
end)
end
end
defp hashtags_changed?(%Object{} = struct, %{"tag" => _} = data) do
Enum.sort(embedded_hashtags(struct)) !=
Enum.sort(object_data_hashtags(data))
end
defp hashtags_changed?(_, _), do: false
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_id_and_maybe_refetch(id, opts \\ []) do
%{updated_at: updated_at} = object = get_by_id(id)
if opts[:interval] &&
NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
case Fetcher.refetch_object(object) do
{:ok, %Object{} = object} ->
object
e ->
Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
object
end
else
object
end
end
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
@doc """
Get a single attachment by it's name and href
"""
@spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil
def get_attachment_by_name_and_href(name, href) do
query =
from(o in Object,
where: fragment("(?)->>'name' = ?", o.data, ^name),
where: fragment("(?)->>'href' = ?", o.data, ^href)
)
Repo.one(query)
end
defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
|> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
def normalize(_, options \\ [fetch: false, id_only: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _), do: object
def normalize(%Activity{object: %Object{} = object}, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, options)
end
# No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, options) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, options)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
def normalize(ap_id, options) when is_binary(ap_id) do
cond do
Keyword.get(options, :id_only) ->
ap_id
Keyword.get(options, :fetch) ->
Fetcher.fetch_object_from_id!(ap_id, options)
true ->
get_cached_by_ap_id(ap_id)
end
end
def normalize(_, _), do: nil
# Owned objects can only be accessed by their owner
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
if actor == ap_id do
:ok
else
{:error, :forbidden}
end
end
# Legacy objects can be accessed by anybody
def authorize_access(%Object{}, %User{}), do: :ok
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
with {:ok, nil} <- @cachex.get(:object_cache, key),
object when not is_nil(object) <- get_by_ap_id(ap_id),
{:ok, true} <- @cachex.put(:object_cache, key, object) do
object
else
{:ok, object} -> object
nil -> nil
end
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
formerType: type,
deleted: deleted
}
|> Map.from_struct()
end
def swap_object_with_tombstone(object) do
tombstone = make_tombstone(object)
with {:ok, object} <-
object
|> Object.change(%{data: tombstone})
|> Repo.update() do
Hashtag.unlink(object)
{:ok, object}
end
end
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
{:ok, _} <- invalid_object_cache(object) do
cleanup_attachments(
Config.get([:instance, :cleanup_attachments]),
- %{"object" => object}
+ %{object: object}
)
{:ok, object, deleted_activity}
end
end
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
{:ok, Oban.Job.t() | nil}
- def cleanup_attachments(true, %{"object" => _} = params) do
+ def cleanup_attachments(true, %{object: _} = params) do
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
end
def cleanup_attachments(_, _), do: {:ok, nil}
def prune(%Object{data: %{"id" => _id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, _} <- invalid_object_cache(object) do
{:ok, object}
end
end
def invalid_object_cache(%Object{data: %{"id" => id}}) do
with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
@cachex.del(:web_resp_cache, URI.parse(id).path)
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
@cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
end
def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do
set_cache(object)
end
end
def increase_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
defp poll_is_multiple?(_), do: false
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id, fetch: false),
"Question" <- object.data["type"] do
key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
options =
object.data[key]
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
data =
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
@doc "Updates data field of an object"
def update_data(%Object{data: data} = object, attrs \\ %{}) do
object
|> Object.change(%{data: Map.merge(data || %{}, attrs)})
|> Repo.update()
end
def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
end
def replies(object, opts \\ []) do
object = Object.normalize(object, fetch: false)
query =
Object
|> where(
[o],
fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
)
|> order_by([o], asc: o.id)
if opts[:self_only] do
actor = object.data["actor"]
where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
else
query
end
end
def self_replies(object, opts \\ []),
do: replies(object, Keyword.put(opts, :self_only, true))
def tags(%Object{data: %{"tag" => tags}}) when is_list(tags), do: tags
def tags(_), do: []
def hashtags(%Object{} = object) do
# Note: always using embedded hashtags regardless whether they are migrated to hashtags table
# (embedded hashtags stay in sync anyways, and we avoid extra joins and preload hassle)
embedded_hashtags(object)
end
def embedded_hashtags(%Object{data: data}) do
object_data_hashtags(data)
end
def embedded_hashtags(_), do: []
def object_data_hashtags(%{"tag" => tags}) when is_list(tags) do
tags
|> Enum.filter(fn
%{"type" => "Hashtag"} = data -> Map.has_key?(data, "name")
plain_text when is_bitstring(plain_text) -> true
_ -> false
end)
|> Enum.map(fn
%{"name" => "#" <> hashtag} -> String.downcase(hashtag)
%{"name" => hashtag} -> String.downcase(hashtag)
hashtag when is_bitstring(hashtag) -> String.downcase(hashtag)
end)
|> Enum.uniq()
# Note: "" elements (plain text) might occur in `data.tag` for incoming objects
|> Enum.filter(&(&1 not in [nil, ""]))
end
def object_data_hashtags(_), do: []
end
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
index e43eef070..75c027137 100644
--- a/lib/pleroma/release_tasks.ex
+++ b/lib/pleroma/release_tasks.ex
@@ -1,73 +1,70 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo
def run(args) do
[task | args] = String.split(args)
case task do
"migrate" -> migrate(args)
"create" -> create()
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
defp mix_task(task, args) do
Application.load(:pleroma)
{:ok, modules} = :application.get_key(:pleroma, :modules)
module =
Enum.find(modules, fn module ->
module = Module.split(module)
match?(["Mix", "Tasks", "Pleroma" | _], module) and
task_match?(module, task)
end)
if module do
module.run(args)
else
IO.puts("The task #{task} does not exist")
end
end
defp task_match?(["Mix", "Tasks", "Pleroma" | module_path], task) do
module_path
|> Enum.join(".")
|> String.downcase()
|> String.equivalent?(String.downcase(task))
end
def migrate(args) do
Mix.Tasks.Pleroma.Ecto.Migrate.run(args)
end
def rollback(args) do
Mix.Tasks.Pleroma.Ecto.Rollback.run(args)
end
def create do
Application.load(:pleroma)
case @repo.__adapter__.storage_up(@repo.config) do
:ok ->
IO.puts("The database for #{inspect(@repo)} has been created")
{:error, :already_up} ->
IO.puts("The database for #{inspect(@repo)} has already been created")
{:error, term} when is_binary(term) ->
- IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
-
- {:error, term} ->
IO.puts(
:stderr,
"The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
)
end
end
end
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
index 59fd5493c..a4fc1ebc2 100644
--- a/lib/pleroma/reverse_proxy/client/tesla.ex
+++ b/lib/pleroma/reverse_proxy/client/tesla.ex
@@ -1,76 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.Tesla do
@behaviour Pleroma.ReverseProxy.Client
@type headers() :: [{String.t(), String.t()}]
@type status() :: pos_integer()
@spec request(atom(), String.t(), headers(), String.t(), keyword()) ::
{:ok, status(), headers}
| {:ok, status(), headers, map()}
| {:error, atom() | String.t()}
| no_return()
@impl true
def request(method, url, headers, body, opts \\ []) do
check_adapter()
opts = Keyword.put(opts, :body_as, :chunks)
with {:ok, response} <-
Pleroma.HTTP.request(
method,
url,
body,
headers,
opts
) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}
else
{:ok, response.status, response.headers}
end
else
{:error, error} -> {:error, error}
end
end
@impl true
@spec stream_body(map()) ::
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
def stream_body(%{pid: _pid, fin: true}) do
:done
end
def stream_body(client) do
case read_chunk!(client) do
{:fin, body} ->
{:ok, body, Map.put(client, :fin, true)}
{:nofin, part} ->
{:ok, part, client}
{:error, error} ->
{:error, error}
end
end
defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
adapter = check_adapter()
adapter.read_chunk(pid, stream, opts)
end
@impl true
@spec close(map) :: :ok | no_return()
def close(%{pid: _pid}) do
+ :ok
end
defp check_adapter do
adapter = Application.get_env(:tesla, :adapter)
adapter
end
end
diff --git a/lib/pleroma/search/elasticsearch.ex b/lib/pleroma/search/elasticsearch.ex
index 16b01101a..20e03e1f0 100644
--- a/lib/pleroma/search/elasticsearch.ex
+++ b/lib/pleroma/search/elasticsearch.ex
@@ -1,95 +1,91 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.Elasticsearch do
@behaviour Pleroma.Search.SearchBackend
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Search.Elasticsearch.Parsers
def es_query(:activity, query, offset, limit) do
must = Parsers.Activity.parse(query)
- if must == [] do
- :skip
- else
- %{
- size: limit,
- from: offset,
- terminate_after: 50,
- timeout: "5s",
- sort: [
- "_score",
- %{"_timestamp" => %{order: "desc", format: "basic_date_time"}}
- ],
- query: %{
- bool: %{
- must: must
- }
+ %{
+ size: limit,
+ from: offset,
+ terminate_after: 50,
+ timeout: "5s",
+ sort: [
+ "_score",
+ %{"_timestamp" => %{order: "desc", format: "basic_date_time"}}
+ ],
+ query: %{
+ bool: %{
+ must: must
}
}
- end
+ }
end
defp maybe_fetch(:activity, search_query) do
with true <- Regex.match?(~r/https?:/, search_query),
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]) do
activity
else
_ -> nil
end
end
def search(user, query, options) do
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
parsed_query =
query
|> String.trim()
|> SearchParser.parse!()
activity_fetch_task =
Task.async(fn ->
maybe_fetch(:activity, String.trim(query))
end)
activity_task =
Task.async(fn ->
q = es_query(:activity, parsed_query, offset, limit)
:activities
|> Pleroma.Search.Elasticsearch.Store.search(q)
|> Enum.filter(fn x ->
x.data["type"] == "Create" && x.object.data["type"] == "Note" &&
Visibility.visible_for_user?(x, user)
end)
end)
activity_results = Task.await(activity_task)
direct_activity = Task.await(activity_fetch_task)
activity_results =
if direct_activity == nil do
activity_results
else
[direct_activity | activity_results]
end
activity_results
end
@impl true
def add_to_index(activity) do
Elasticsearch.put_document(Pleroma.Search.Elasticsearch.Cluster, activity, "activities")
end
@impl true
def remove_from_index(object) do
Elasticsearch.delete_document(Pleroma.Search.Elasticsearch.Cluster, object, "activities")
end
end
diff --git a/lib/pleroma/search/elasticsearch/document_mappings/activity.ex b/lib/pleroma/search/elasticsearch/document_mappings/activity.ex
index b77c6e0d2..71ef75634 100644
--- a/lib/pleroma/search/elasticsearch/document_mappings/activity.ex
+++ b/lib/pleroma/search/elasticsearch/document_mappings/activity.ex
@@ -1,61 +1,61 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defimpl Elasticsearch.Document, for: Pleroma.Activity do
alias Pleroma.Object
require Pleroma.Constants
def id(obj), do: obj.id
def routing(_), do: false
def object_to_search_data(object) do
# Only index public or unlisted Notes
if not is_nil(object) and object.data["type"] == "Note" and
not is_nil(object.data["content"]) and
(Pleroma.Constants.as_public() in object.data["to"] or
Pleroma.Constants.as_public() in object.data["cc"]) and
String.length(object.data["content"]) > 1 do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
trimmed <- String.trim(scrubbed) do
trimmed
end
if String.length(content) > 1 and not is_nil(data["published"]) do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
_timestamp: published,
content: content,
instance: URI.parse(object.data["actor"]).host,
hashtags: Object.hashtags(object),
user: Pleroma.User.get_cached_by_ap_id(object.data["actor"]).nickname
}
else
%{}
end
else
%{}
end
end
def encode(activity) do
object = Pleroma.Object.normalize(activity)
object_to_search_data(object)
end
end
defimpl Elasticsearch.Document, for: Pleroma.Object do
def id(obj), do: obj.id
def routing(_), do: false
- def encode(_), do: nil
+ def encode(_), do: %{}
end
diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex
index c36b8f751..8fcf9310a 100644
--- a/lib/pleroma/search/meilisearch.ex
+++ b/lib/pleroma/search/meilisearch.ex
@@ -1,169 +1,170 @@
defmodule Pleroma.Search.Meilisearch do
require Logger
require Pleroma.Constants
alias Pleroma.Activity
import Pleroma.Search.DatabaseSearch
import Ecto.Query
@behaviour Pleroma.Search.SearchBackend
defp meili_headers do
private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
[{"Content-Type", "application/json"}] ++
if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
end
def meili_get(path) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.get(
Path.join(endpoint, path),
meili_headers()
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_post(path, params) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.post(
Path.join(endpoint, path),
Jason.encode!(params),
meili_headers()
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_put(path, params) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.request(
:put,
Path.join(endpoint, path),
Jason.encode!(params),
meili_headers(),
[]
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_delete!(path) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
{:ok, _} =
Pleroma.HTTP.request(
:delete,
Path.join(endpoint, path),
"",
meili_headers(),
[]
)
end
def search(user, query, options \\ []) do
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
res =
meili_post(
"/indexes/objects/search",
%{q: query, offset: offset, limit: limit}
)
with {:ok, result} <- res do
hits = result["hits"] |> Enum.map(& &1["ap"])
try do
hits
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object()
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
|> maybe_fetch(user, query)
|> order_by([object: obj], desc: obj.data["published"])
|> Pleroma.Repo.all()
rescue
_ -> maybe_fetch([], user, query)
end
end
end
def object_to_search_data(object) do
# Only index public or unlisted Notes
if not is_nil(object) and object.data["type"] == "Note" and
not is_nil(object.data["content"]) and
(Pleroma.Constants.as_public() in object.data["to"] or
Pleroma.Constants.as_public() in object.data["cc"]) and
String.length(object.data["content"]) > 1 do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
trimmed <- String.trim(scrubbed) do
trimmed
end
if String.length(content) > 1 and not is_nil(data["published"]) do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
id: object.id,
content: content,
ap: data["id"],
published: published |> DateTime.to_unix()
}
end
end
end
@impl true
def add_to_index(activity) do
maybe_search_data = object_to_search_data(activity.object)
if activity.data["type"] == "Create" and maybe_search_data do
result =
meili_put(
"/indexes/objects/documents",
[maybe_search_data]
)
with {:ok, res} <- result,
true <- Map.has_key?(res, "taskUid") do
- # Do nothing
+ {:ok, res}
else
- _ ->
+ err ->
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
+ {:error, err}
end
end
end
@impl true
def remove_from_index(object) do
meili_delete!("/indexes/objects/documents/#{object.id}")
end
end
diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex
index ed6bfd329..56e3b7de5 100644
--- a/lib/pleroma/search/search_backend.ex
+++ b/lib/pleroma/search/search_backend.ex
@@ -1,17 +1,17 @@
defmodule Pleroma.Search.SearchBackend do
@doc """
Add the object associated with the activity to the search index.
The whole activity is passed, to allow filtering on things such as scope.
"""
- @callback add_to_index(activity :: Pleroma.Activity.t()) :: nil
+ @callback add_to_index(activity :: Pleroma.Activity.t()) :: {:ok, any()} | {:error, any()}
@doc """
Remove the object from the index.
Just the object, as opposed to the whole activity, is passed, since the object
is what contains the actual content and there is no need for fitlering when removing
from index.
"""
- @callback remove_from_index(object :: Pleroma.Object.t()) :: nil
+ @callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()}
end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index 043a0643e..1c59be9c7 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -1,80 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@known_suffixes ["/publickey", "/main-key"]
def key_id_to_actor_id(key_id) do
uri =
key_id
|> URI.parse()
|> Map.put(:fragment, nil)
|> remove_suffix(@known_suffixes)
maybe_ap_id = URI.to_string(uri)
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
{:ok, ap_id} ->
{:ok, ap_id}
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
- %{"ap_id" => ap_id} -> {:ok, ap_id}
+ {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end
end
defp remove_suffix(uri, [test | rest]) do
if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
Map.put(uri, :path, String.replace(uri.path, test, ""))
else
remove_suffix(uri, rest)
end
end
defp remove_suffix(uri, []), do: uri
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%User{keys: keys} = user, headers) do
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 9bf8e03df..3b5419db7 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,269 +1,269 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload do
@moduledoc """
Manage user uploads
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
* `:description`: upload alternative text
* `:base_url`: override base url
* `:uploader`: override uploader
* `:filters`: override filters
* `:size_limit`: override size limit
* `:activity_type`: override activity type
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
* `:id` - the upload id.
* `:name` - the upload file name.
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
* `:width` - width of the media in pixels
* `:height` - height of the media in pixels
* `:blurhash` - string hash of the image encoded with the blurhash algorithm (https://blurha.sh/)
Related behaviors:
* `Pleroma.Uploaders.Uploader`
* `Pleroma.Upload.Filter`
"""
alias Ecto.UUID
alias Pleroma.Config
alias Pleroma.Maps
alias Pleroma.Web.ActivityPub.Utils
require Logger
@type source ::
Plug.Upload.t()
| (data_uri_string :: String.t())
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
| map()
@type option ::
{:type, :avatar | :banner | :background}
| {:description, String.t()}
| {:activity_type, String.t()}
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
tempfile: String.t(),
content_type: String.t(),
width: integer(),
height: integer(),
blurhash: String.t(),
path: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
defp get_description(opts, upload) do
case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
_ -> ""
end
end
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
opts = get_opts(opts)
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = get_description(opts, upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"id" => Utils.generate_object_id(),
"type" => opts.activity_type,
"mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
|> Maps.put_if_present("width", upload.width)
|> Maps.put_if_present("height", upload.height)
],
"name" => description
}
|> Maps.put_if_present("blurhash", upload.blurhash)}
else
{:description_limit, _} ->
{:error, :description_too_long}
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
)
{:error, error}
end
end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
:banner ->
{Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
:background ->
{Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
_ ->
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end
%{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
base_url: base_url()
}
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
with :ok <- check_file_size(file.path, opts.size_limit) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: file.filename,
tempfile: file.path,
content_type: file.content_type
}}
end
end
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
- hash = Base.encode16(:crypto.hash(:sha256, data), lower: true)
+ hash = Base.encode16(:crypto.hash(:sha256, data), case: :lower)
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
{:ok, %{mime_type: content_type}} <-
Majic.perform({:bytes, data}, pool: Pleroma.MajicPool),
[ext | _] <- MIME.extensions(content_type) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: hash <> "." <> ext,
tempfile: tmp_path,
content_type: content_type
}}
end
end
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
end
defp check_binary_size(binary, size_limit)
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
{:error, :file_too_large}
end
defp check_binary_size(_, _), do: :ok
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
with {:ok, %{size: size}} <- File.stat(path),
true <- size <= size_limit do
:ok
else
false -> {:error, :file_too_large}
error -> error
end
end
defp check_file_size(_, _), do: :ok
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
# automatically.
defp tempfile_for_image(data) do
{:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
IO.binwrite(tmp_file, data)
tmp_path
end
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""
end
[base_url, path]
|> Path.join()
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
def base_url do
uploader = Config.get([Pleroma.Upload, :uploader])
upload_base_url = Config.get([Pleroma.Upload, :base_url])
public_endpoint = Config.get([uploader, :public_endpoint])
case uploader do
Pleroma.Uploaders.Local ->
upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
Pleroma.Uploaders.S3 ->
bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
truncated_namespace = Config.get([Pleroma.Uploaders.S3, :truncated_namespace])
namespace = Config.get([Pleroma.Uploaders.S3, :bucket_namespace])
bucket_with_namespace =
cond do
!is_nil(truncated_namespace) ->
truncated_namespace
!is_nil(namespace) ->
namespace <> ":" <> bucket
true ->
bucket
end
if public_endpoint do
Path.join([public_endpoint, bucket_with_namespace])
else
Path.join([upload_base_url, bucket_with_namespace])
end
_ ->
public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
end
end
end
diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex
index c89c30fc1..8990e97c0 100644
--- a/lib/pleroma/upload/filter/analyze_metadata.ex
+++ b/lib/pleroma/upload/filter/analyze_metadata.ex
@@ -1,83 +1,82 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
@moduledoc """
Extracts metadata about the upload, such as width/height
"""
require Logger
@behaviour Pleroma.Upload.Filter
@spec filter(Pleroma.Upload.t()) ::
{:ok, :filtered, Pleroma.Upload.t()} | {:ok, :noop} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do
try do
image =
file
|> Mogrify.open()
|> Mogrify.verbose()
upload =
upload
|> Map.put(:width, image.width)
|> Map.put(:height, image.height)
|> Map.put(:blurhash, get_blurhash(file))
{:ok, :filtered, upload}
rescue
e in ErlangError ->
Logger.warn("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
end
def filter(%Pleroma.Upload{tempfile: file, content_type: "video" <> _} = upload) do
try do
result = media_dimensions(file)
upload =
upload
|> Map.put(:width, result.width)
|> Map.put(:height, result.height)
{:ok, :filtered, upload}
rescue
e in ErlangError ->
Logger.warn("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
end
def filter(_), do: {:ok, :noop}
defp get_blurhash(file) do
with {:ok, blurhash} <- :eblurhash.magick(file) do
blurhash
else
_ -> nil
end
end
defp media_dimensions(file) do
with executable when is_binary(executable) <- System.find_executable("ffprobe"),
args = [
"-v",
"error",
"-show_entries",
"stream=width,height",
"-of",
"csv=p=0:s=x",
file
],
{result, 0} <- System.cmd(executable, args),
[width, height] <-
String.split(String.trim(result), "x") |> Enum.map(&String.to_integer(&1)) do
%{width: width, height: height}
else
nil -> {:error, {:ffprobe, :command_not_found}}
- {:error, _} = error -> error
end
end
end
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex
index a2bfbbf61..9e82cf8a7 100644
--- a/lib/pleroma/upload/filter/exiftool.ex
+++ b/lib/pleroma/upload/filter/exiftool.ex
@@ -1,31 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exiftool do
@moduledoc """
Strips GPS related EXIF tags and overwrites the file in place.
Also strips or replaces filesystem metadata e.g., timestamps.
"""
@behaviour Pleroma.Upload.Filter
- @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+ @spec filter(Pleroma.Upload.t()) :: {:ok, :noop} | {:ok, :filtered} | {:error, String.t()}
# Formats not compatible with exiftool at this time
def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
{_response, 0} -> {:ok, :filtered}
{error, 1} -> {:error, error}
end
rescue
e in ErlangError ->
{:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
def filter(_), do: {:ok, :noop}
end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
index 01126aaeb..69885a0bd 100644
--- a/lib/pleroma/upload/filter/mogrifun.ex
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -1,53 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Mogrifun do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload.Filter
@moduledoc """
This module is just an example of an Upload filter. It's not supposed to be used in production.
"""
@filters [
{"implode", "1"},
{"-raise", "20"},
{"+raise", "20"},
[{"-interpolate", "nearest"}, {"-virtual-pixel", "mirror"}, {"-spread", "5"}],
"+polaroid",
{"-statistic", "Mode 10"},
{"-emboss", "0x1.1"},
{"-emboss", "0x2"},
{"-colorspace", "Gray"},
"-negate",
[{"-channel", "green"}, "-negate"],
[{"-channel", "red"}, "-negate"],
[{"-channel", "blue"}, "-negate"],
{"+level-colors", "green,gold"},
{"+level-colors", ",DodgerBlue"},
{"+level-colors", ",Gold"},
{"+level-colors", ",Lime"},
{"+level-colors", ",Red"},
{"+level-colors", ",DarkGreen"},
{"+level-colors", "firebrick,yellow"},
{"+level-colors", "'rgb(102,75,25)',lemonchiffon"},
[{"fill", "red"}, {"tint", "40"}],
[{"fill", "green"}, {"tint", "40"}],
[{"fill", "blue"}, {"tint", "40"}],
[{"fill", "yellow"}, {"tint", "40"}]
]
- @spec filter(Pleroma.Upload.t()) :: {:ok, atom()} | {:error, String.t()}
+ @spec filter(Pleroma.Upload.t()) :: {:ok, :filtered | :noop} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
{:ok, :filtered}
rescue
e in ErlangError ->
{:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
def filter(_), do: {:ok, :noop}
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index ba30769bb..d7c1511ce 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,2617 +1,2646 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
+ @moduledoc """
+ A user, local or remote
+ """
+
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.Hashtag
alias Pleroma.User.HashtagFollow
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.Endpoint
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
| :approval_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
]
]
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "users" do
field(:bio, :string, default: "")
field(:raw_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, default: %{})
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:featured_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(:is_locked, :boolean, default: false)
field(:is_confirmed, :boolean, default: true)
field(:password_reset_pending, :boolean, default: false)
field(:is_approved, :boolean, default: true)
field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
field(:is_active, :boolean, default: true)
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(:mastofe_settings, :map, default: nil)
field(:uri, ObjectValidators.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(: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(:is_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, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{})
field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime)
field(:language, :string)
field(:status_ttl_days, :integer, default: nil)
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)
has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
many_to_many(:followed_hashtags, Hashtag,
on_replace: :delete,
on_delete: :delete_all,
join_through: HashtagFollow
)
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
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
on_replace: :delete
)
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
target_users_query
|> User.Query.build(%{deactivated: false})
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
def cached_blocked_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
blocked_users_ap_ids(user)
end)
end
def cached_muted_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
muted_users_ap_ids(user)
end)
end
defdelegate following_count(user), to: FollowingRelationship
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@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{is_active: false}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{local: true, is_approved: false}), do: :approval_pending
def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
def account_status(%User{}), do: :active
@spec visible_for(User.t(), User.t() | nil) ::
:visible
| :invisible
| :restricted_unauthenticated
| :deactivated
| :confirmation_pending
def visible_for(user, for_user \\ nil)
def visible_for(%User{invisible: true}, _), do: :invisible
def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
def visible_for(%User{} = user, nil) do
if restrict_unauthenticated?(user) do
:restrict_unauthenticated
else
visible_account_status(user)
end
end
def visible_for(%User{} = user, for_user) do
if superuser?(for_user) do
:visible
else
visible_account_status(user)
end
end
def visible_for(_, _), do: :invisible
defp restrict_unauthenticated?(%User{local: true}) do
Config.restrict_unauthenticated_access?(:profiles, :local)
end
defp restrict_unauthenticated?(%User{local: _}) do
Config.restrict_unauthenticated_access?(:profiles, :remote)
end
defp visible_account_status(user) do
status = account_status(user)
if status in [:active, :password_reset_pending] do
:visible
else
status
end
end
@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
_ ->
unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
end
end
end
def banner_url(user, options \\ []) do
case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
end
end
# Should probably be renamed or removed
@spec ap_id(User.t()) :: String.t()
def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
@spec ap_followers(User.t()) :: String.t()
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 ap_featured_collection(User.t()) :: String.t()
def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
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 = Config.get([:instance, :user_bio_length], 5000)
name_limit = 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,
:emoji,
:ap_id,
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
:is_locked,
:last_refreshed_at,
:uri,
:follower_address,
:following_address,
:featured_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:is_discoverable,
:invisible,
:actor_type,
:also_known_as,
:pinned_objects
]
)
|> cast(params, [:name], empty_values: [])
|> validate_required([:ap_id])
|> validate_required([:name], trim: false)
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
|> validate_non_local()
end
defp validate_non_local(cng) do
local? = get_field(cng, :local)
if local? do
cng
|> add_error(:local, "User is local, can't update with this changeset.")
else
cng
end
end
def update_changeset(struct, params \\ %{}) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
struct
|> cast(
params,
[
:bio,
:raw_bio,
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:is_locked,
:no_rich_text,
:default_scope,
:banner,
:hide_follows,
:hide_followers,
:hide_followers_count,
:hide_follows_count,
:hide_favorites,
:allow_following_move,
:also_known_as,
:background,
:show_role,
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store,
:is_discoverable,
:actor_type,
:disclose_client,
:status_ttl_days
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, ["Person", "Service"])
|> validate_number(:status_ttl_days, greater_than: 0)
|> 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
+ # These fields are inconsistent in tests when it comes to binary/atom keys
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
+ |> Enum.map(fn
+ %{name: name, value: value} ->
+ %{"name" => name, "value" => value}
+
+ %{"name" => _} = field ->
+ field
+ end)
|> 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
emojified_fields = [:bio, :name, :raw_fields]
if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
emoji = Map.merge(bio, name)
emoji =
changeset
|> get_field(:raw_fields)
|> Enum.reduce(emoji, fn x, acc ->
Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
end)
put_change(changeset, :emoji, emoji)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
with {:ok, value} <- fetch_change(changeset, map_field),
{:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
{:error, :file_too_large} ->
Ecto.Changeset.validate_change(changeset, map_field, fn map_field, _value ->
[{map_field, "file is too large"}]
end)
_ ->
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)
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, 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, 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)
# Used to auto-register LDAP accounts which won't have a password hash stored locally
def register_changeset_ldap(struct, params = %{password: password})
when is_nil(password) do
params =
if Map.has_key?(params, :email) do
Map.put_new(params, :email, params[:email])
else
params
end
struct
|> cast(params, [
:name,
:nickname,
:email
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
|> put_private_key()
end
- def register_changeset(struct, params \\ %{}, opts \\ []) do
+ @spec register_changeset(User.t(), map(), keyword()) :: Changeset.t()
+ def register_changeset(%User{} = struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
confirmed? =
if is_nil(opts[:confirmed]) do
!Config.get([:instance, :account_activation_required])
else
opts[:confirmed]
end
approved? =
if is_nil(opts[:approved]) do
!Config.get([:instance, :account_approval_required])
else
opts[:approved]
end
struct
|> confirmation_changeset(set_confirmation: confirmed?)
|> approval_changeset(set_approval: approved?)
|> cast(params, [
:bio,
:raw_bio,
:email,
:name,
:nickname,
:password,
:password_confirmation,
:emoji,
:registration_reason,
:language
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_change(:email, fn :email, email ->
valid? =
Config.get([User, :email_blacklist])
|> Enum.all?(fn blacklisted_domain ->
!String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
end)
if valid?, do: [], else: [email: "Invalid email"]
end)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
|> put_private_key()
end
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
end
end
def put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
def put_following_and_follower_and_featured_address(changeset) do
user = %User{nickname: get_field(changeset, :nickname)}
followers = ap_followers(user)
following = ap_following(user)
featured = ap_featured_collection(user)
changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
|> put_change(:featured_address, featured)
end
defp put_private_key(changeset) do
{:ok, pem} = Keys.generate_rsa_pem()
put_change(changeset, :keys, pem)
end
defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, is_active: true})
|> Repo.all()
follow_all(user, autofollowed_users)
end
defp autofollowing_users(user) do
candidates = Config.get([:instance, :autofollowing_nicknames])
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
|> Enum.each(&follow(&1, user, :follow_accept))
{:ok, :success}
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
+ @spec register(Changeset.t()) :: {:ok, User.t()} | {:error, any} | nil
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
+ @spec post_register_action(User.t()) :: {:error, any} | {:ok, User.t()}
def post_register_action(%User{is_confirmed: false} = user) do
with {:ok, _} <- maybe_send_confirmation_email(user) do
{:ok, user}
end
end
def post_register_action(%User{is_approved: false} = user) do
with {:ok, _} <- send_user_approval_email(user),
{:ok, _} <- send_admin_approval_emails(user) do
{:ok, user}
end
end
def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- maybe_send_registration_email(user),
{:ok, _} <- maybe_send_welcome_email(user),
{:ok, _} <- maybe_send_welcome_message(user) do
{:ok, user}
end
end
defp send_user_approval_email(user) do
user
|> Pleroma.Emails.UserEmail.approval_pending_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
end
defp send_admin_approval_emails(user) do
all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, :enqueued}
end
defp maybe_send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
defp maybe_send_welcome_email(_), do: {:ok, :noop}
@spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
when is_binary(email) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def maybe_send_confirmation_email(_), do: {:ok, :noop}
@spec send_confirmation_email(Uset.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
user
end
@spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
with false <- User.WelcomeEmail.enabled?(),
false <- Config.get([:instance, :account_activation_required], false),
false <- Config.get([:instance, :account_approval_required], false) do
user
|> Pleroma.Emails.UserEmail.successful_registration_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
else
_ ->
{:ok, :noop}
end
end
defp maybe_send_registration_email(_), do: {:ok, :noop}
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()}
+ @spec maybe_direct_follow(User.t(), User.t()) ::
+ {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_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, followed}
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
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
not followed.is_active ->
{: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)
end
end
def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"}
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
error ->
error
end
end
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_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)
nil ->
{:error, "Not subscribed!"}
end
end
@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.is_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
+ @spec set_cache(
+ {:error, any}
+ | {:ok, User.t()}
+ | User.t()
+ ) :: {:ok, User.t()} | {:error, any}
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
+ @spec update_and_set_cache(User.t(), map()) :: {:ok, User.t()} | {:error, any}
def update_and_set_cache(struct, params) do
struct
|> update_changeset(params)
|> update_and_set_cache()
end
+ @spec update_and_set_cache(Changeset.t()) :: {:ok, User.t()} | {:error, any}
def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user)
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
|> maybe_remove_report_notifications(was_superuser_before_update)
end
defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
if not User.superuser?(user),
do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
result
end
defp maybe_remove_report_notifications(result, _) do
result
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}")
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, "muted_users_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
+ @spec get_cached_by_id(String.t()) :: nil | Pleroma.User.t()
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 = 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
@spec get_by_nickname(String.t()) :: User.t() | nil
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, is_active: true})
end
def get_followers_query(%User{} = 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{} = user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(%User{} = 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{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(%User{} = 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{} = 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{} = user), do: get_friends_query(user, nil)
def get_friends(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ap_ids(%User{} = user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
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
@spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
if user.local or !Config.get([:instance, :external_user_synchronization]) do
follower_count = FollowingRelationship.follower_count(user)
user
|> follow_information_changeset(%{follower_count: follower_count})
|> update_and_set_cache
else
{:ok, maybe_fetch_follow_information(user)}
end
end
@spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
if Config.get([:instance, :external_user_synchronization]) do
{:ok, maybe_fetch_follow_information(user)}
else
{:ok, 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})
|> update_and_set_cache()
end
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, is_active: true}
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]
query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
query
|> Repo.all()
end
@spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
notifications? = Map.get(params, :notifications, true)
expires_in = Map.get(params, :expires_in, 0)
with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
{:ok, user_notification_mute} <-
(notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
{:ok, nil} do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_user",
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
schedule_in: expires_in
)
end
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
end
def unmute(%User{} = muter, %User{} = mutee) do
with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(muter, mutee) do
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, [user_mute, user_notification_mute]}
end
end
def unmute(muter_id, mutee_id) do
with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
{:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
unmute(muter, mutee)
else
{who, result} = error ->
Logger.warn(
"User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
)
{:error, error}
end
end
def subscribe(%User{} = subscriber, %User{} = target) do
deny_follow_blocked = 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 from both sides as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
blocker =
case CommonAPI.reject_follow_request(blocker, blocked) do
{:ok, %User{} = updated_blocker} -> updated_blocker
nil -> blocker
end
unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
if unfollowing_blocked && 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 set_activation_async(user, status \\ true) do
BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
end
@spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def set_activation(users, status) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: set_activation(user, status)
end)
end
@spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def set_activation(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
end
def approve(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- approve(user), do: user
end)
end)
end
def approve(%User{is_approved: false} = user) do
with chg <- change(user, is_approved: true),
{:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end
def approve(%User{} = user), do: {:ok, user}
def confirm(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- confirm(user), do: user
end)
end)
end
def confirm(%User{is_confirmed: false} = user) do
with chg <- confirmation_changeset(user, set_confirmation: true),
{:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end
def confirm(%User{} = user), do: {:ok, user}
def set_suggestion(users, is_suggested) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- set_suggestion(user, is_suggested), do: user
end)
end)
end
def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
user
|> change(is_suggested: is_suggested)
|> update_and_set_cache()
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
@spec purge_user_changeset(User.t()) :: Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
is_locked: false,
password_reset_pending: false,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
is_active: false,
ap_enabled: false,
is_moderator: false,
is_admin: false,
mastofe_settings: nil,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
is_discoverable: false,
also_known_as: []
# id: preserved
# ap_id: preserved
# nickname: preserved
})
end
# Purge doesn't delete the user from the database.
# It just nulls all its fields and deactivates it.
# See `User.purge_user_changeset/1` above.
defp purge(%User{} = user) do
user
|> purge_user_changeset()
|> 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
# Purge the user immediately
purge(user)
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
# *Actually* delete the user from the DB
defp delete_from_db(%User{} = user) do
invalidate_cache(user)
Repo.delete(user)
end
# If the user never finalized their account, it's safe to delete them.
defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
do: delete_from_db(user)
defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
do: delete_from_db(user)
defp maybe_delete_from_db(user), do: {:ok, user}
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
# Purge the user again, in case perform/2 is called directly
purge(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)
delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user)
maybe_delete_from_db(user)
end
def perform(:set_activation_async, user, status), do: set_activation(user, status)
@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 delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|> Repo.delete_all()
end
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> Repo.chunk_stream(50, :batches)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
{:ok, delete_data, _} <- Builder.delete(user, object) do
Pipeline.common_pipeline(delete_data, local: user.local)
else
{:find_object, nil} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete_activity(activity, user)
end
e ->
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
Logger.error("Error: #{inspect(e)}")
end
end
defp delete_activity(%{data: %{"type" => type}} = activity, user)
when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity)
Pipeline.common_pipeline(undo, local: user.local)
end
defp delete_activity(_activity, _user), do: "Doing nothing"
defp delete_outgoing_pending_follow_requests(user) do
user
|> FollowingRelationship.outgoing_pending_follow_requests_query()
|> Repo.delete_all()
end
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: 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
cached_user = get_cached_by_ap_id(ap_id)
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
case {cached_user, maybe_fetched_user} do
{_, {:ok, %User{} = user}} ->
{:ok, user}
{%User{} = user, _} ->
{:ok, user}
e ->
Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
{:error, :not_found}
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
|> put_private_key()
|> 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("https://" <> _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
def local_nickname_regex do
if 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(%User{} = user) do
if String.contains?(user.nickname, "@") do
user.nickname
else
%{host: host} = URI.parse(user.ap_id)
user.nickname <> "@" <> host
end
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, is_active: true})
|> 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.is_active == ^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 set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def set_confirmation(%User{} = user, bool) do
user
|> confirmation_changeset(set_confirmation: bool)
|> update_and_set_cache()
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 = Config.get([:assets, :mascots])
default_mascot = 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 get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(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])
|> maybe_validate_required_email(false)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
def alias_users(user) do
user.also_known_as
|> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user != nil end)
end
def add_alias(user, new_alias_user) do
current_aliases = user.also_known_as || []
new_alias_ap_id = new_alias_user.ap_id
if new_alias_ap_id in current_aliases do
{:ok, user}
else
user
|> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
|> update_and_set_cache()
end
end
+ @spec delete_alias(User.t(), User.t()) :: {:error, :no_such_alias}
def delete_alias(user, alias_user) do
current_aliases = user.also_known_as || []
alias_ap_id = alias_user.ap_id
if alias_ap_id in current_aliases do
user
|> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
|> update_and_set_cache()
else
{:error, :no_such_alias}
end
end
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, status) do
user
|> cast(%{is_active: status}, [:is_active])
|> 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 validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = 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 = Config.get([:instance, :account_field_name_length], 255)
value_limit = 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, Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
String.split_at(value, 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(%{mastofe_settings: settings}, [:mastofe_settings])
|> validate_required([:mastofe_settings])
|> update_and_set_cache()
end
@spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
def confirmation_changeset(user, set_confirmation: confirmed?) do
params =
if confirmed? do
%{
is_confirmed: true,
confirmation_token: nil
}
else
%{
is_confirmed: false,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
}
end
cast(user, params, [:is_confirmed, :confirmation_token])
end
- @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
+ @spec approval_changeset(Changeset.t(), keyword()) :: Changeset.t()
def approval_changeset(user, set_approval: approved?) do
cast(user, %{is_approved: approved?}, [:is_approved])
end
@spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
def add_pinned_object_id(%User{} = user, object_id) do
if !user.pinned_objects[object_id] do
params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
user
|> cast(params, [:pinned_objects])
|> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
if Enum.count(pinned_objects) <= max_pinned_statuses do
[]
else
[pinned_objects: "You have already pinned the maximum number of statuses"]
end
end)
else
change(user)
end
|> update_and_set_cache()
end
@spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
def remove_pinned_object_id(%User{} = user, object_id) do
user
|> cast(
%{pinned_objects: Map.delete(user.pinned_objects, object_id)},
[:pinned_objects]
)
|> 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
with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
+ else
+ err -> err
end
end
- @spec add_to_block(User.t(), User.t()) ::
+ @spec remove_from_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
+ else
+ err -> err
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
def get_host(%User{ap_id: ap_id} = _user) do
URI.parse(ap_id).host
end
def update_last_active_at(%__MODULE__{local: true} = user) do
user
|> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
|> update_and_set_cache()
end
def active_user_count(days \\ 30) do
active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
__MODULE__
|> where([u], u.last_active_at >= ^active_after)
|> where([u], u.local == true)
|> Repo.aggregate(:count)
end
def update_last_status_at(user) do
User
|> where(id: ^user.id)
|> update([u], set: [last_status_at: fragment("NOW()")])
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
when is_list(follows),
do: user
defp maybe_load_followed_hashtags(%User{} = user) do
followed_hashtags = HashtagFollow.get_by_user(user)
%{user | followed_hashtags: followed_hashtags}
end
def followed_hashtags(%User{followed_hashtags: follows})
when is_list(follows),
do: follows
def followed_hashtags(%User{} = user) do
{:ok, user} =
user
|> maybe_load_followed_hashtags()
|> set_cache()
user.followed_hashtags
end
def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
user = maybe_load_followed_hashtags(user)
with {:ok, _} <- HashtagFollow.new(user, hashtag),
follows <- HashtagFollow.get_by_user(user),
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
user
|> set_cache()
end
end
def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
user = maybe_load_followed_hashtags(user)
with {:ok, _} <- HashtagFollow.delete(user, hashtag),
follows <- HashtagFollow.get_by_user(user),
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
user
|> set_cache()
end
end
def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
not is_nil(HashtagFollow.get(user, hashtag))
end
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index 2c6378265..63709a484 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -1,242 +1,243 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Backup do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Pleroma.Web.Gettext
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Workers.BackupWorker
schema "backups" do
field(:content_type, :string)
field(:file_name, :string)
field(:file_size, :integer, default: 0)
field(:processed, :boolean, default: false)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
def create(user, admin_id \\ nil) do
with :ok <- validate_limit(user, admin_id),
{:ok, backup} <- user |> new() |> Repo.insert() do
BackupWorker.process(backup, admin_id)
end
end
def new(user) do
rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
%__MODULE__{
user_id: user.id,
content_type: "application/zip",
file_name: name
}
end
def delete(backup) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do
Repo.delete(backup)
end
end
defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok
defp validate_limit(user, nil) do
case get_last(user.id) do
%__MODULE__{inserted_at: inserted_at} ->
days = Pleroma.Config.get([__MODULE__, :limit_days])
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
if diff > days do
:ok
else
{:error,
dngettext(
"errors",
"Last export was less than a day ago",
"Last export was less than %{days} days ago",
days,
days: days
)}
end
nil ->
:ok
end
end
def get_last(user_id) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> limit(1)
|> Repo.one()
end
def list(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> Repo.all()
end
def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> where([b], b.id != ^latest_id)
|> Repo.all()
|> Enum.each(&BackupWorker.delete/1)
end
def get(id), do: Repo.get(__MODULE__, id)
def process(%__MODULE__{} = backup) do
with {:ok, zip_file} <- export(backup),
{:ok, %{size: size}} <- File.stat(zip_file),
{:ok, _upload} <- upload(backup, zip_file) do
backup
|> cast(%{file_size: size, processed: true}, [:file_size, :processed])
|> Repo.update()
end
end
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
def export(%__MODULE__{} = backup) do
backup = Repo.preload(backup, :user)
name = String.trim_trailing(backup.file_name, ".zip")
dir = dir(name)
with :ok <- File.mkdir(dir),
:ok <- actor(dir, backup.user),
:ok <- statuses(dir, backup.user),
:ok <- likes(dir, backup.user),
:ok <- bookmarks(dir, backup.user),
- {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
+ {:ok, zip_path} <-
+ :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: String.to_charlist(dir)),
{:ok, _} <- File.rm_rf(dir) do
{:ok, to_string(zip_path)}
end
end
def dir(name) do
dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!()
Path.join(dir, name)
end
def upload(%__MODULE__{} = backup, zip_path) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
upload = %Pleroma.Upload{
name: backup.file_name,
tempfile: zip_path,
content_type: backup.content_type,
path: Path.join("backups", backup.file_name)
}
with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
:ok <- File.rm(zip_path) do
{:ok, upload}
end
end
defp actor(dir, user) do
with {:ok, json} <-
UserView.render("user.json", %{user: user})
|> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
|> Jason.encode() do
File.write(Path.join(dir, "actor.json"), json)
end
end
defp write_header(file, name) do
IO.write(
file,
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "#{name}.json",
"type": "OrderedCollection",
"orderedItems": [
"""
)
end
defp write(query, dir, name, fun) do
path = Path.join(dir, "#{name}.json")
with {:ok, file} <- File.open(path, [:write, :utf8]),
:ok <- write_header(file, name) do
total =
query
|> Pleroma.Repo.chunk_stream(100)
|> Enum.reduce(0, fn i, acc ->
with {:ok, data} <- fun.(i),
{:ok, str} <- Jason.encode(data),
:ok <- IO.write(file, str <> ",\n") do
acc + 1
else
_ -> acc
end
end)
with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
File.close(file)
end
end
end
defp bookmarks(dir, %{id: user_id} = _user) do
Bookmark
|> where(user_id: ^user_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
|> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
end
defp likes(dir, user) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Like")
|> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
|> write(dir, "likes", fn a -> {:ok, a.object} end)
end
defp statuses(dir, user) do
opts =
%{}
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:actor_id, user.ap_id)
[
[Pleroma.Constants.as_public(), user.ap_id],
User.following(user),
Pleroma.List.memberships(user)
]
|> Enum.concat()
|> ActivityPub.fetch_activities_query(opts)
|> write(dir, "outbox", fn a ->
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
{:ok, Map.delete(activity, "@context")}
end
end)
end
end
diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex
index 5761e3b38..24560d4a3 100644
--- a/lib/pleroma/web.ex
+++ b/lib/pleroma/web.ex
@@ -1,242 +1,245 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web do
@moduledoc """
A module that keeps using definitions for controllers,
views and so on.
This can be used in your application as:
use Pleroma.Web, :controller
use Pleroma.Web, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below.
"""
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.PlugHelper
def controller do
quote do
use Phoenix.Controller, namespace: Pleroma.Web
import Plug.Conn
import Pleroma.Web.Gettext
import Pleroma.Web.TranslationHelpers
alias Pleroma.Web.Router.Helpers, as: Routes
plug(:set_put_layout)
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
# Marks plugs intentionally skipped and blocks their execution if present in plugs chain
defp skip_plug(conn, plug_modules) do
plug_modules
|> List.wrap()
|> Enum.reduce(
conn,
fn plug_module, conn ->
try do
plug_module.skip_plug(conn)
rescue
UndefinedFunctionError ->
- raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
+ reraise(
+ "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code.",
+ __STACKTRACE__
+ )
end
end
)
end
defp skip_auth(conn, _) do
skip_plug(conn, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug])
end
defp skip_public_check(conn, _) do
skip_plug(conn, EnsurePublicOrAuthenticatedPlug)
end
# Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do
with %{halted: false} = conn <-
maybe_drop_authentication_if_oauth_check_ignored(conn),
%{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
%{halted: false} = conn <- maybe_perform_authenticated_check(conn),
%{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
super(conn, params)
end
end
# For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
# (neither performed nor explicitly skipped)
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
AuthHelper.drop_auth_info(conn)
else
conn
end
end
# Ensures instance is public -or- user is authenticated if such check was scheduled
defp maybe_perform_public_or_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
EnsurePublicOrAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Ensures user is authenticated if such check was scheduled
# Note: runs prior to action even if it was already executed earlier in plug chain
# (since OAuthScopesPlug has option of proceeding unauthenticated)
defp maybe_perform_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
EnsureAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
defp maybe_halt_on_missing_oauth_scopes_check(conn) do
if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
"Security violation: OAuth scopes check was neither handled nor explicitly skipped."
)
|> halt()
else
conn
end
end
end
end
def view do
quote do
use Phoenix.View,
root: "lib/pleroma/web/templates",
namespace: Pleroma.Web
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
import Pleroma.Web.ErrorHelpers
import Pleroma.Web.Gettext
alias Pleroma.Web.Router.Helpers, as: Routes
require Logger
@doc "Same as `render/3` but wrapped in a rescue block"
def safe_render(view, template, assigns \\ %{}) do
Phoenix.View.render(view, template, assigns)
rescue
error ->
Logger.error(
"#{__MODULE__} failed to render #{inspect({view, template})}\n" <>
Exception.format(:error, error, __STACKTRACE__)
)
nil
end
@doc """
Same as `render_many/4` but wrapped in rescue block.
"""
def safe_render_many(collection, view, template, assigns \\ %{}) do
Enum.map(collection, fn resource ->
as = Map.get(assigns, :as) || view.__resource__
assigns = Map.put(assigns, as, resource)
safe_render(view, template, assigns)
end)
|> Enum.filter(& &1)
end
end
end
def router do
quote do
use Phoenix.Router
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Phoenix.Channel
import Pleroma.Web.Gettext
end
end
def plug do
quote do
@behaviour Pleroma.Web.Plug
@behaviour Plug
@doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
"""
def skip_plug(conn) do
PlugHelper.append_to_private_list(
conn,
PlugHelper.skipped_plugs_list_id(),
__MODULE__
)
end
@impl Plug
@doc """
Before-plug hook that
* ensures the plug is not skipped
* processes `:if_func` / `:unless_func` functional pre-run conditions
* adds plug to the list of called plugs and calls `perform/2` if checks are passed
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) ||
(options[:if_func] && !options[:if_func].(conn)) ||
(options[:unless_func] && options[:unless_func].(conn)) do
conn
else
conn =
PlugHelper.append_to_private_list(
conn,
PlugHelper.called_plugs_list_id(),
__MODULE__
)
apply(__MODULE__, :perform, [conn, options])
end
end
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 3adb4ab54..8e55df0d8 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1863 +1,1868 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Akkoma.Collections
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Hashtag
alias Pleroma.Maps
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.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
alias Pleroma.Workers.PollWorker
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Persisting
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Streaming
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_can_insert(%{"type" => "Delete"}), do: true
defp check_actor_can_insert(%{"type" => "Undo"}), do: true
defp check_actor_can_insert(%{"actor" => actor}) when is_binary(actor) do
case User.get_cached_by_ap_id(actor) do
%User{is_active: true} -> true
_ -> false
end
end
defp check_actor_can_insert(_), do: true
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 update_last_status_at_if_public(actor, object) do
if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
end
defp 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
defp increase_replies_count_if_reply(_create_data), do: :noop
@object_types ~w[Question Answer Audio Video Event Article Note Page]
@impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do
{:ok, object, meta}
end
end
@unpersisted_activity_types ~w[Undo Delete Remove Accept Reject]
@impl true
def persist(%{"type" => type} = object, [local: false] = meta)
when type in @unpersisted_activity_types do
{:ok, object, meta}
{recipients, _, _} = get_recipients(object)
unpersisted = %Activity{
data: object,
local: false,
recipients: recipients,
actor: object["actor"]
}
{:ok, unpersisted, meta}
end
@impl true
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"]
}),
# TODO: add tests for expired activities, when Note type will be supported in new pipeline
{:ok, _} <- maybe_create_activity_expiration(activity) 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} <- {:actor_check, bypass_actor_check || check_actor_can_insert(map)},
{_, true} <- {:remote_limit_pass, 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),
{:ok, activity} <- insert_activity_with_expiration(map, local, recipients) do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
end)
# Add local posts to search index
if local, do: Pleroma.Search.add_to_index(activity)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:actor_check, _} ->
{:error, false}
{:containment, _} = error ->
error
{:error, _} = error ->
error
{: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}
{:remote_limit_pass, _} ->
{:error, :remote_limit}
{:reject, _} = e ->
{:error, e}
end
end
defp insert_activity_with_expiration(data, local, recipients) do
struct = %Activity{
data: data,
local: local,
actor: data["actor"],
recipients: recipients
}
with {:ok, activity} <- Repo.insert(struct) do
maybe_create_activity_expiration(activity)
end
end
def notify_and_stream(activity) do
Notification.create_notifications(activity)
original_activity =
case activity do
%{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} ->
Activity.get_create_by_object_ap_id_with_object(id)
_ ->
activity
end
conversation = create_or_bump_conversation(original_activity, original_activity.actor)
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
end
defp maybe_create_activity_expiration(
%{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
) do
with {:ok, _job} <-
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: activity.id,
expires_at: expires_at
}) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(activity), do: {:ok, activity}
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) do
Participation.mark_as_read(user, conversation)
{: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
@impl true
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
conversation = Repo.preload(conversation, :participations)
last_activity_id =
fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
user: user,
blocking_user: user
})
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
@impl true
def stream_out_participations(_, _), do: :noop
@impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete", "Update"] do
activity
|> Topics.get_activity_topics()
|> Streamer.stream(activity)
end
@impl true
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
create_data =
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
)
with {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(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
defp maybe_schedule_poll_notifications(activity) do
PollWorker.schedule_poll_end(activity)
:ok
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)
defp do_unfollow(follower, followed, activity_id, local) when local == true do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
{:ok, _activity} <- Repo.delete(follow_activity),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
defp do_unfollow(follower, followed, activity_id, false) do
# On a remote unfollow, _remove_ their activity from the database, since some software (MISSKEEEEY)
# uses deterministic ids for follows.
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, _activity} <- Repo.delete(follow_activity),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
unfollow_activity <- make_unfollow_activity(unfollow_data, false),
_ <- notify_and_stream(unfollow_activity) do
{:ok, unfollow_activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
defp make_unfollow_activity(data, local) do
{recipients, _, _} = get_recipients(data)
%Activity{
data: data,
local: local,
actor: data["actor"],
recipients: recipients
}
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(params) do
with {:ok, result} <- Repo.transaction(fn -> do_flag(params) end) do
result
end
end
defp do_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),
_ <- notify_and_stream(activity),
:ok <-
maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> user.ap_id != actor end)
|> 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}
else
{:error, error} -> Repo.rollback(error)
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,
"to" => [origin.follower_address]
}
with true <- origin.ap_id in target.also_known_as,
{:ok, activity} <- insert(params, local),
_ <- notify_and_stream(activity) 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_blockers_visibility(opts)
|> restrict_recipients(recipients, opts[:user])
|> restrict_filtered(opts)
|> 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_direct_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
|> restrict_visibility(%{visibility: "direct"})
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
defp fetch_paginated_optimized(query, opts, pagination) do
# Note: tag-filtering funcs may apply "ORDER BY objects.id DESC",
# and extra sorting on "activities.id DESC NULLS LAST" would worse the query plan
opts = Map.put(opts, :skip_extra_order, true)
Pagination.fetch_paginated(query, opts, pagination)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts[:user])
fetch_activities_query(recipients ++ list_memberships, opts)
|> fetch_paginated_optimized(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts[:user])
end
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
includes_local_public = Map.get(opts, :includes_local_public, false)
opts = Map.delete(opts, :user)
intended_recipients =
if includes_local_public do
[Constants.as_public(), as_local_public()]
else
[Constants.as_public()]
end
intended_recipients
|> fetch_activities_query(opts)
|> restrict_unlisted(opts)
|> fetch_paginated_optimized(opts, pagination)
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts
|> Map.put(:restrict_unlisted, true)
|> fetch_public_or_unlisted_activities(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
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
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
local_public = as_local_public()
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)
)
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)
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params)
|> Enum.reverse()
end
def fetch_user_activities(user, reading_user, params \\ %{})
def fetch_user_activities(user, reading_user, %{total: true} = params) do
result = fetch_activities_for_user(user, reading_user, params)
Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_user_activities(user, reading_user, params) do
user
|> fetch_activities_for_user(reading_user, params)
|> Enum.reverse()
end
defp fetch_activities_for_user(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_object_ids, Map.keys(user.pinned_objects))
params =
if User.blocks?(reading_user, user) do
params
else
params
|> Map.put(:blocking_user, reading_user)
|> Map.put(:muting_user, reading_user)
end
pagination_type = Map.get(params, :pagination_type) || :keyset
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params, pagination_type)
end
def fetch_statuses(reading_user, %{total: true} = params) do
result = fetch_activities_for_reading_user(reading_user, params)
Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_statuses(reading_user, params) do
reading_user
|> fetch_activities_for_reading_user(params)
|> Enum.reverse()
end
defp fetch_activities_for_reading_user(reading_user, params) do
params = Map.put(params, :type, ["Create", "Announce"])
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params, :offset)
end
def user_activities_recipients(%{godmode: true}), do: []
def user_activities_recipients(%{reading_user: reading_user}) do
if not is_nil(reading_user) and reading_user.local do
[
Constants.as_public(),
as_local_public(),
reading_user.ap_id | User.following(reading_user)
]
else
[Constants.as_public()]
end
end
defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'type' != ? or ?->>'actor' != ?",
activity.data,
"Announce",
object.data,
^actor
)
)
end
defp restrict_announce_object_actor(query, _), do: query
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_embedded_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_embedded_tag_all(query, %{tag_all: tag}) when is_binary(tag) do
restrict_embedded_tag_any(query, %{tag: tag})
end
defp restrict_embedded_tag_all(query, _), do: query
defp restrict_embedded_tag_any(_query, %{tag: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_any(query, %{tag: [_ | _] = tag_any}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag_any)
)
end
defp restrict_embedded_tag_any(query, %{tag: tag}) when is_binary(tag) do
restrict_embedded_tag_any(query, %{tag: [tag]})
end
defp restrict_embedded_tag_any(query, _), do: query
defp restrict_embedded_tag_reject_any(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_reject_any(query, %{tag_reject: [_ | _] = tag_reject}) do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_embedded_tag_reject_any(query, %{tag_reject: tag_reject})
when is_binary(tag_reject) do
restrict_embedded_tag_reject_any(query, %{tag_reject: [tag_reject]})
end
defp restrict_embedded_tag_reject_any(query, _), do: query
defp object_ids_query_for_tags(tags) do
from(hto in "hashtags_objects")
|> join(:inner, [hto], ht in Pleroma.Hashtag, on: hto.hashtag_id == ht.id)
|> where([hto, ht], ht.name in ^tags)
|> select([hto], hto.object_id)
|> distinct([hto], true)
end
defp restrict_hashtag_all(_query, %{tag_all: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_all(query, %{tag_all: [single_tag]}) do
restrict_hashtag_any(query, %{tag: single_tag})
end
defp restrict_hashtag_all(query, %{tag_all: [_ | _] = tags}) do
from(
[_activity, object] in query,
where:
fragment(
"""
(SELECT array_agg(hashtags.name) FROM hashtags JOIN hashtags_objects
ON hashtags_objects.hashtag_id = hashtags.id WHERE hashtags.name = ANY(?)
AND hashtags_objects.object_id = ?) @> ?
""",
^tags,
object.id,
^tags
)
)
end
defp restrict_hashtag_all(query, %{tag_all: tag}) when is_binary(tag) do
restrict_hashtag_all(query, %{tag_all: [tag]})
end
defp restrict_hashtag_all(query, _), do: query
defp restrict_hashtag_any(_query, %{tag: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_any(query, %{tag: [_ | _] = tags}) do
hashtag_ids =
from(ht in Hashtag, where: ht.name in ^tags, select: ht.id)
|> Repo.all()
# Note: NO extra ordering should be done on "activities.id desc nulls last" for optimal plan
from(
[_activity, object] in query,
join: hto in "hashtags_objects",
on: hto.object_id == object.id,
where: hto.hashtag_id in ^hashtag_ids,
distinct: [desc: object.id],
order_by: [desc: object.id]
)
end
defp restrict_hashtag_any(query, %{tag: tag}) when is_binary(tag) do
restrict_hashtag_any(query, %{tag: [tag]})
end
defp restrict_hashtag_any(query, _), do: query
defp restrict_hashtag_reject_any(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_reject_any(query, %{tag_reject: [_ | _] = tags_reject}) do
from(
[_activity, object] in query,
where: object.id not in subquery(object_ids_query_for_tags(tags_reject))
)
end
defp restrict_hashtag_reject_any(query, %{tag_reject: tag_reject}) when is_binary(tag_reject) do
restrict_hashtag_reject_any(query, %{tag_reject: [tag_reject]})
end
defp restrict_hashtag_reject_any(query, _), do: query
defp raise_on_missing_preload do
raise "Can't use the child object without preloading!"
end
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
# Essentially, either look for activities addressed to `recipients`, _OR_ ones
# that reference a hashtag that the user follows
# Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
# follow any
defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
restrict_recipients(query, recipients, user)
end
defp restrict_recipients_or_hashtags(query, recipients, user, []) do
restrict_recipients(query, recipients, user)
end
defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
from([activity, object] in query)
|> join(:left, [activity, object], hto in "hashtags_objects",
on: hto.object_id == object.id,
as: :hto
)
|> where(
[activity, object, hto: hto],
(hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
fragment("? && ?", ^recipients, activity.recipients)
)
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_remote(query, %{remote: true}) do
from(activity in query, where: activity.local == false)
end
defp restrict_remote(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: true}) do
from(
[activity, object] in query,
where: fragment("(?)->>'type' = ?", activity.data, "Create"),
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{exclude_replies: true}) do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
defp restrict_replies(query, %{
reply_filtering_user: %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{} = user,
reply_visibility: "following"
}) do
from(
[activity, object] in query,
where:
fragment(
"""
?->>'type' != 'Create' -- This isn't a Create
OR ?->>'inReplyTo' is null -- this isn't a reply
OR ? && array_remove(?, ?) -- The recipient is us or one of our friends,
-- unless they are the author (because authors
-- are also part of the recipients). This leads
-- to a bug that self-replies by friends won't
-- show up.
OR ? = ? -- The actor is us
""",
activity.data,
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: true}) do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{with_muted: true}), 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' \\?| ?) or ? = ?",
activity.data,
^mutes,
activity.actor,
^user.ap_id
)
)
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,
# You don't block the author
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
# You don't block any recipients, and didn't author the post
where:
fragment(
"((not (? && ?)) or ? = ?)",
activity.recipients,
^blocked_ap_ids,
activity.actor,
^user.ap_id
),
# You don't block the domain of any recipients, and didn't author the post
where:
fragment(
"(recipients_contain_blocked_domains(?, ?) = false) or ? = ?",
activity.recipients,
^domain_blocks,
activity.actor,
^user.ap_id
),
# It's not a boost of a user you block
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocked_ap_ids
),
# You don't block the author's domain, and also don't follow the author
where:
fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
activity.actor,
^domain_blocks,
activity.actor,
^following_ap_ids
),
# Same as above, but checks the Object
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_blockers_visibility(query, %{blocking_user: %User{} = user}) do
if Config.get([:activitypub, :blockers_visible]) == true do
query
else
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
from(
activity in query,
# The author doesn't block you
where: fragment("not (? = ANY(?))", activity.actor, ^blocker_ap_ids),
# It's not a boost of a user that blocks you
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocker_ap_ids
)
)
end
end
defp restrict_blockers_visibility(query, _), do: query
defp restrict_unlisted(query, %{restrict_unlisted: true}) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^[Constants.as_public()]
)
)
end
defp restrict_unlisted(query, _), do: query
defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do
from(
[activity, object: o] in query,
where:
fragment(
"(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)",
activity.data,
activity.data,
activity.data,
^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}) when is_binary(instance) do
from(
activity in query,
where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance)
)
end
defp restrict_instance(query, %{instance: instance}) when is_list(instance) do
from(
activity in query,
where: fragment("split_part(actor::text, '/'::text, 3) = ANY(?)", ^instance)
)
end
defp restrict_instance(query, _), do: query
defp restrict_filtered(query, %{user: %User{} = user}) do
case Filter.compose_regex(user) do
nil ->
query
regex ->
from([activity, object] in query,
where:
fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
activity.actor == ^user.ap_id
)
end
end
defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
restrict_filtered(query, %{user: user})
end
defp restrict_filtered(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_invisible_actors(query, %{type: "Flag"}), do: query
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do
query
|> join(:inner, [activity], u in User,
as: :u,
on: activity.actor == u.ap_id and u.invisible == false
)
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 normalize_fetch_activities_query_opts(opts) do
Enum.reduce([:tag, :tag_all, :tag_reject], opts, fn key, opts ->
case opts[key] do
value when is_bitstring(value) ->
Map.put(opts, key, Hashtag.normalize_name(value))
value when is_list(value) ->
normalized_value =
value
|> Enum.map(&Hashtag.normalize_name/1)
|> Enum.uniq()
Map.put(opts, key, normalized_value)
_ ->
opts
end
end)
end
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 =
if opts[:blocking_user] && opts[:blocking_user] == source_user do
[:block | ap_id_relationships]
else
ap_id_relationships
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
opts = normalize_fetch_activities_query_opts(opts)
{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])
}
query =
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_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_remote(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_blockers_visibility(opts)
|> restrict_muted(restrict_muted_opts)
|> restrict_filtered(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)
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
if Config.feature_enabled?(:improved_hashtag_timeline) do
query
|> restrict_hashtag_any(opts)
|> restrict_hashtag_all(opts)
|> restrict_hashtag_reject_any(opts)
else
query
|> restrict_embedded_tag_any(opts)
|> restrict_embedded_tag_all(opts)
|> restrict_embedded_tag_reject_any(opts)
end
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, pagination_id: like.id})
|> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{skip_order: true}),
pagination
)
end
defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
Enum.map(activities, fn
%{data: %{"bcc" => [_ | _] = bcc}} = activity ->
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
defp 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 = Maps.put_if_present(data, "actor", opts[:actor])
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 normalize_image(%{"url" => url}) do
%{
"type" => "Image",
"url" => [%{"href" => url}]
}
end
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil
+ defp normalize_also_known_as(aka) when is_list(aka), do: aka
+ defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
+ defp normalize_also_known_as(nil), do: []
+
defp object_to_user_data(data, additional) do
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)
|> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
is_locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
end
shared_inbox =
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
data["endpoints"]["sharedInbox"]
end
# if WebFinger request was already done, we probably have acct, otherwise
# we request WebFinger here
nickname = additional[:nickname_from_acct] || generate_nickname(data)
# also_known_as must be a URL
also_known_as =
data
|> Map.get("alsoKnownAs", [])
+ |> normalize_also_known_as()
|> Enum.filter(fn url ->
case URI.parse(url) do
%URI{scheme: "http"} -> true
%URI{scheme: "https"} -> true
_ -> false
end
end)
%{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
ap_enabled: true,
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: also_known_as,
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
pinned_objects: pinned_objects,
nickname: nickname
}
end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
generated = "#{username}@#{URI.parse(data["id"]).host}"
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
case WebFinger.finger(generated) do
{:ok, %{"subject" => "acct:" <> acct}} -> acct
_ -> generated
end
else
generated
end
end
# nickname can be nil because of virtual actors
defp generate_nickname(_), do: nil
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
def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
{_, 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} ->
user_data
e ->
Logger.error(
"Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
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, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do
{:ok, object_to_user_data(data, additional)}
else
e -> {:error, e}
end
end
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data, additional) do
{:ok, maybe_update_follow_information(data)}
else
# If this has been deleted, only log a debug and not an error
{:error, {"Object has been deleted", _, _} = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
{:error, {:reject, reason} = e} ->
Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
{:error, e}
{:error, e} ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
end
end
def maybe_handle_clashing_nickname(data) do
with nickname when is_binary(nickname) <- data[:nickname],
%User{} = old_user <- User.get_by_nickname(nickname),
{_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
Logger.info(
"Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{data[:ap_id]}, renaming."
)
old_user
|> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
|> User.update_and_set_cache()
else
{:ap_id_comparison, true} ->
Logger.info(
"Found an old user for #{data[:nickname]}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
)
_ ->
nil
end
end
def pin_data_from_featured_collection(%{
"type" => "OrderedCollection",
"first" => first
}) do
with {:ok, page} <- Fetcher.fetch_and_contain_remote_object_from_id(first) do
page
|> Map.get("orderedItems")
|> Map.new(fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end)
else
e ->
Logger.error("Could not decode featured collection at fetch #{first}, #{inspect(e)}")
{:ok, %{}}
end
end
def pin_data_from_featured_collection(
%{
"type" => type
} = collection
)
when type in ["OrderedCollection", "Collection"] do
{:ok, objects} = Collections.Fetcher.fetch_collection(collection)
# Items can either be a map _or_ a string
objects
|> Map.new(fn
ap_id when is_binary(ap_id) -> {ap_id, NaiveDateTime.utc_now()}
%{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()}
end)
end
def fetch_and_prepare_featured_from_ap_id(nil) do
{:ok, %{}}
end
def fetch_and_prepare_featured_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
{:ok, pin_data_from_featured_collection(data)}
else
e ->
Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}")
{:ok, %{}}
end
end
def pinned_fetch_task(nil), do: nil
def pinned_fetch_task(%{pinned_objects: pins}) do
if Enum.all?(pins, fn {ap_id, _} ->
Object.get_cached_by_ap_id(ap_id) ||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
end) do
:ok
else
:error
end
end
def make_user_from_ap_id(ap_id, additional \\ []) 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, additional) do
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
if user do
user
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
else
maybe_handle_clashing_nickname(data)
data
|> User.remote_user_changeset()
|> Repo.insert()
|> User.set_cache()
end
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <-
WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id, nickname_from_acct: acct)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
# filter out broken threads
defp 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
defp maybe_restrict_deactivated_users(activity, %{type: "Flag"}), do: activity
defp maybe_restrict_deactivated_users(activity, _opts),
do: Activity.restrict_deactivated_users(activity)
end
diff --git a/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex
index ca9b4b64a..022da3198 100644
--- a/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex
+++ b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex
@@ -1,45 +1,51 @@
defmodule Pleroma.Web.AkkomaAPI.TranslationController do
use Pleroma.Web, :controller
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ require Logger
+
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:statuses"]}
when action in [
:languages
]
)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TranslationOperation
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/akkoma/translation/languages"
def languages(conn, _params) do
with {:enabled, true} <- {:enabled, Pleroma.Config.get([:translator, :enabled])},
{:ok, source_languages, dest_languages} <- get_languages() do
conn
|> json(%{source: source_languages, target: dest_languages})
else
- {:enabled, false} -> json(conn, %{})
- e -> IO.inspect(e)
+ {:enabled, false} ->
+ json(conn, %{})
+
+ e ->
+ Logger.error("Translation language list error: #{inspect(e)}")
+ {:error, e}
end
end
defp get_languages do
module = Pleroma.Config.get([:translator, :module])
@cachex.fetch!(:translations_cache, "languages:#{module}}", fn _ ->
with {:ok, source_languages, dest_languages} <- module.languages() do
{:ok, source_languages, dest_languages}
else
{:error, err} -> {:ignore, {:error, err}}
end
end)
end
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index 49247d9b6..c38349486 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -1,329 +1,338 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def remote_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Make request to another instance for emoji packs list",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [
url_param(),
Operation.parameter(
:page,
:query,
%Schema{type: :integer, default: 1},
"Page"
),
Operation.parameter(
:page_size,
:query,
%Schema{type: :integer, default: 30},
"Number of emoji to return"
)
],
operationId: "PleromaAPI.EmojiPackController.remote",
responses: %{
200 => emoji_packs_response(),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def index_operation do
%Operation{
tags: ["Emoji packs"],
summary: "Lists local custom emoji packs",
operationId: "PleromaAPI.EmojiPackController.index",
parameters: [
Operation.parameter(
:page,
:query,
%Schema{type: :integer, default: 1},
"Page"
),
Operation.parameter(
:page_size,
:query,
%Schema{type: :integer, default: 50},
"Number of emoji packs to return"
)
],
responses: %{
200 => emoji_packs_response()
}
}
end
def show_operation do
%Operation{
tags: ["Emoji packs"],
summary: "Show emoji pack",
operationId: "PleromaAPI.EmojiPackController.show",
parameters: [
name_param(),
Operation.parameter(
:page,
:query,
%Schema{type: :integer, default: 1},
"Page"
),
Operation.parameter(
:page_size,
:query,
%Schema{type: :integer, default: 30},
"Number of emoji to return"
)
],
responses: %{
200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def archive_operation do
%Operation{
tags: ["Emoji packs"],
summary: "Requests a local pack archive from the instance",
operationId: "PleromaAPI.EmojiPackController.archive",
parameters: [name_param()],
responses: %{
200 =>
Operation.response("Archive file", "application/octet-stream", %Schema{
type: :string,
format: :binary
}),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def download_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Download pack from another instance",
operationId: "PleromaAPI.EmojiPackController.download",
security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", download_request(), required: true),
responses: %{
200 => ok_response(),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp download_request do
%Schema{
type: :object,
required: [:url, :name],
properties: %{
url: %Schema{
type: :string,
format: :uri,
description: "URL of the instance to download from"
},
name: %Schema{type: :string, format: :uri, description: "Pack Name"},
as: %Schema{type: :string, format: :uri, description: "Save as"}
}
}
end
def create_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Create an empty pack",
operationId: "PleromaAPI.EmojiPackController.create",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
400 => Operation.response("Not Found", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Delete a custom emoji pack",
operationId: "PleromaAPI.EmojiPackController.delete",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Updates (replaces) pack metadata",
operationId: "PleromaAPI.EmojiPackController.update",
security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", update_request(), required: true),
parameters: [name_param()],
responses: %{
200 => Operation.response("Metadata", "application/json", metadata()),
400 => Operation.response("Bad Request", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def import_from_filesystem_operation do
%Operation{
tags: ["Emoji pack administration"],
summary: "Imports packs from filesystem",
operationId: "PleromaAPI.EmojiPackController.import",
security: [%{"oAuth" => ["admin:write"]}],
responses: %{
200 =>
Operation.response("Array of imported pack names", "application/json", %Schema{
type: :array,
items: %Schema{type: :string}
})
}
}
end
defp name_param do
Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true)
end
defp url_param do
Operation.parameter(
:url,
:query,
%Schema{type: :string, format: :uri},
"URL of the instance",
required: true
)
end
defp ok_response do
Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
end
defp emoji_packs_response do
Operation.response(
"Object with pack names as keys and pack contents as values",
"application/json",
%Schema{
type: :object,
- additionalProperties: emoji_pack(),
+ properties: %{
+ count: %Schema{type: :integer},
+ packs: %Schema{
+ type: :object,
+ additionalProperties: emoji_pack()
+ }
+ },
example: %{
- "emojos" => emoji_pack().example
+ "count" => 4,
+ "packs" => %{
+ "emojos" => emoji_pack().example
+ }
}
}
)
end
defp emoji_pack do
%Schema{
title: "EmojiPack",
type: :object,
properties: %{
files: files_object(),
pack: %Schema{
type: :object,
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"can-download": %Schema{type: :boolean},
"share-files": %Schema{type: :boolean},
"download-sha256": %Schema{type: :string}
}
}
},
example: %{
"files" => %{"emacs" => "emacs.png", "guix" => "guix.png"},
"pack" => %{
"license" => "Test license",
"homepage" => "https://pleroma.social",
"description" => "Test description",
"can-download" => true,
"share-files" => true,
"download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
}
}
}
end
defp files_object do
%Schema{
type: :object,
additionalProperties: %Schema{type: :string},
description: "Object with emoji names as keys and filenames as values"
}
end
defp update_request do
%Schema{
type: :object,
properties: %{
metadata: %Schema{
type: :object,
description: "Metadata to replace the old one",
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"fallback-src": %Schema{
type: :string,
format: :uri,
description: "Fallback url to download pack from"
},
"fallback-src-sha256": %Schema{
type: :string,
description: "SHA256 encoded for fallback pack archive"
},
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
}
}
}
}
end
defp metadata do
%Schema{
type: :object,
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"fallback-src": %Schema{
type: :string,
format: :uri,
description: "Fallback url to download pack from"
},
"fallback-src-sha256": %Schema{
type: :string,
description: "SHA256 encoded for fallback pack archive"
},
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
}
}
end
end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index 68472e75f..bb377d686 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -1,104 +1,108 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Plugs.AuthenticationPlug
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)},
{:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do
{:ok, user}
else
{:error, _reason} = error -> error
error -> {:error, error}
end
end
@doc """
Gets or creates Pleroma.Registration record from Ueberauth assigns.
Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response —
see [`docs/config.md`](docs/config.md).
"""
def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}),
do: {:error, :missing_uid}
def get_registration(%Plug.Conn{
assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
}) do
registration = Registration.get_by_provider_uid(provider, uid)
if registration do
{:ok, registration}
else
info = auth.info
%Registration{}
|> Registration.changeset(%{
provider: to_string(provider),
uid: to_string(uid),
info: %{
"nickname" => info.nickname,
"email" => info.email,
"name" => info.name,
"description" => info.description
}
})
|> Repo.insert()
end
end
def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
@doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."
+ @spec create_from_registration(Plug.Conn.t(), Registration.t()) ::
+ {:ok, User.t()} | {:error, any()}
def create_from_registration(
%Plug.Conn{params: %{"authorization" => registration_attrs}},
%Registration{} = registration
) do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])
name = value([registration_attrs["name"], Registration.name(registration)]) || nickname
bio = value([registration_attrs["bio"], Registration.description(registration)]) || ""
random_password = :crypto.strong_rand_bytes(64) |> Base.encode64()
with {:ok, new_user} <-
User.register_changeset(
%User{},
%{
email: email,
nickname: nickname,
name: name,
bio: bio,
password: random_password,
password_confirmation: random_password
},
external: true,
confirmed: true
)
|> Repo.insert(),
{:ok, _} <-
Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do
{:ok, new_user}
+ else
+ err -> err
end
end
defp value(list), do: Enum.find(list, &(to_string(&1) != ""))
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
def oauth_consumer_template, do: nil
end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 7b84b43e4..6acc8f078 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -1,118 +1,120 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
alias Pleroma.Pagination
alias Pleroma.Web.Utils.Params
def json_response(conn, status, _) when status in [204, :no_content] do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(status, "")
end
def json_response(conn, status, json) do
conn
|> put_status(status)
|> json(json)
end
@spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
def fetch_integer_param(params, name, default \\ nil) do
params
|> Map.get(name, default)
|> param_to_integer(default)
end
defp param_to_integer(val, _) when is_integer(val), do: val
defp param_to_integer(val, default) when is_binary(val) do
case Integer.parse(val) do
{res, _} -> res
_ -> default
end
end
defp param_to_integer(_, default), do: default
def add_link_headers(conn, entries, extra_params \\ %{})
def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _entries, _extra_params),
do: conn
def add_link_headers(conn, entries, extra_params) do
case get_pagination_fields(conn, entries, extra_params) do
%{"next" => next_url, "prev" => prev_url} ->
put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
_ ->
conn
end
end
@id_keys Pagination.page_keys() -- ["limit", "order"]
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
params =
conn.params
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
%{
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
"prev" => current_url(conn, Map.put(params, :min_id, min_id)),
"id" => current_url(conn)
}
end
def get_pagination_fields(conn, entries, extra_params \\ %{}) do
case List.last(entries) do
%{pagination_id: max_id} when not is_nil(max_id) ->
%{pagination_id: min_id} = List.first(entries)
build_pagination_fields(conn, min_id, max_id, extra_params)
%{id: max_id} ->
%{id: min_id} = List.first(entries)
build_pagination_fields(conn, min_id, max_id, extra_params)
_ ->
%{}
end
end
def assign_account_by_id(conn, _) do
case Pleroma.User.get_cached_by_id(conn.params.id) do
- %Pleroma.User{} = account -> assign(conn, :account, account)
- nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+ %Pleroma.User{} = account ->
+ assign(conn, :account, account)
+
+ nil ->
+ Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found})
+ |> halt()
end
end
+ @spec try_render(Plug.Conn.t(), any, any) :: Plug.Conn.t()
def try_render(conn, target, params) when is_binary(target) do
- case render(conn, target, params) do
- nil -> render_error(conn, :not_implemented, "Can't display this activity")
- res -> res
- end
+ render(conn, target, params)
end
def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity")
end
@doc """
Returns true if request specifies to include embedded relationships in account objects.
May only be used in selected account-related endpoints; has no effect for status- or
notification-related endpoints.
"""
# Intended for PleromaFE: https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838
def embed_relationships?(params) do
# To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])`
params
|> Map.get(:with_relationships, params["with_relationships"])
|> Params.truthy_param?()
end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 6dd66a424..baf0c5651 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,181 +1,181 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma
require Pleroma.Constants
alias Pleroma.Config
socket("/live", Phoenix.LiveView.Socket)
plug(Pleroma.Web.Plugs.SetLocalePlug)
plug(CORSPlug)
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
# Cache-control headers are duplicated in case we turn off etags in the future
plug(
Pleroma.Web.Plugs.InstanceStatic,
at: "/",
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
cache_control_for_etags: "public, max-age=1209600",
headers: %{
"cache-control" => "public, max-age=1209600"
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
# Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/akkoma/swaggerui")
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/akkoma/swaggerui",
frontend_type: :swagger,
gzip: true,
if: &Pleroma.Web.Swagger.ui_enabled?/0,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :mastodon,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/",
from: :pleroma,
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static,
at: "/pleroma/admin/",
from: {:pleroma, "priv/static/adminfe/"}
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug(Phoenix.CodeReloader)
end
plug(Pleroma.Web.Plugs.TrailingFormatPlug)
plug(Plug.RequestId)
plug(Plug.Logger, log: :debug)
plug(Plug.Parsers,
parsers: [
:urlencoded,
- {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
+ Pleroma.Web.Plugs.Parsers.Multipart,
:json
],
pass: ["*/*"],
json_decoder: Jason,
length: Config.get([:instance, :upload_limit]),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
plug(Plug.MethodOverride)
plug(Plug.Head)
secure_cookies = Config.get([__MODULE__, :secure_cookie_flag])
cookie_name =
if secure_cookies,
do: "__Host-pleroma_key",
else: "pleroma_key"
extra =
Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: cookie_name,
signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
http_only: true,
secure: secure_cookies,
extra: extra
)
plug(Pleroma.Web.Plugs.RemoteIp)
plug(Pleroma.Web.Router)
@doc """
Dynamically loads configuration from the system environment
on startup.
It receives the endpoint configuration from the config files
and must return the updated configuration.
"""
def load_from_system_env(config) do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
end
def websocket_url do
String.replace_leading(url(), "http", "ws")
end
end
diff --git a/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex b/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex
deleted file mode 100644
index f86d6b52b..000000000
--- a/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex
+++ /dev/null
@@ -1,26 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Fallback.LegacyPleromaApiRerouterPlug do
- alias Pleroma.Web.Endpoint
- alias Pleroma.Web.Fallback.RedirectController
-
- def init(opts), do: opts
-
- def call(%{path_info: ["api", "pleroma" | path_info_rest]} = conn, _opts) do
- new_path_info = ["api", "v1", "pleroma" | path_info_rest]
- new_request_path = Enum.join(new_path_info, "/")
-
- conn
- |> Map.merge(%{
- path_info: new_path_info,
- request_path: new_request_path
- })
- |> Endpoint.call(conn.params)
- end
-
- def call(conn, _opts) do
- RedirectController.api_not_implemented(conn, %{})
- end
-end
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 8990bef54..7308726f5 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -1,66 +1,70 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.Activity
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.HTML
defp scrub_html_and_truncate_object_field(field, object) do
field
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> Activity.HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Emoji.Formatter.demojify()
|> HtmlEntities.decode()
|> Formatter.truncate()
end
def scrub_html_and_truncate(%{data: %{"summary" => summary}} = object)
when is_binary(summary) and summary != "" do
summary
|> scrub_html_and_truncate_object_field(object)
end
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
content
|> scrub_html_and_truncate_object_field(object)
end
+ def scrub_html_and_truncate(%{data: _}) do
+ ""
+ end
+
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
content
|> scrub_html
|> Emoji.Formatter.demojify()
|> HtmlEntities.decode()
|> Formatter.truncate(max_length)
end
def scrub_html(content) when is_binary(content) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
end
def scrub_html(content), do: content
def user_name_string(user) do
"#{user.name} " <>
if user.local do
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
else
"(@#{user.nickname})"
end
end
@spec fetch_media_type(list(String.t()), String.t()) :: String.t() | nil
def fetch_media_type(supported_types, media_type) do
Enum.find(supported_types, fn support_type ->
String.starts_with?(media_type, support_type)
end)
end
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 455af11d7..8f32e7219 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -1,661 +1,660 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.MFAView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.Utils.Params
require Logger
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
plug(:fetch_session)
plug(:fetch_flash)
plug(:skip_auth)
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
if Params.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
handle_existing_authorization(conn, params)
end
end
# Note: the token is set in oauth_plug, but the token and client do not always go together.
# For example, MastodonFE's token is set if user requests with another client,
# after user already authorized to MastodonFE.
# So we have to check client and token.
def authorize(
%Plug.Conn{assigns: %{token: %Token{} = token, user: %User{} = user}} = conn,
%{"client_id" => client_id} = params
) do
with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
^client_id <- t.app.client_id do
handle_existing_authorization(conn, params)
else
_ ->
maybe_reuse_token(conn, params, user.id)
end
end
def authorize(%Plug.Conn{} = conn, params) do
# if we have a user in the session, attempt to authenticate as them
# otherwise show the login form
maybe_reuse_token(conn, params, AuthHelper.get_session_user(conn))
end
defp maybe_reuse_token(conn, params, user_id) when is_binary(user_id) do
with %User{} = user <- User.get_cached_by_id(user_id),
%App{} = app <- Repo.get_by(App, client_id: params["client_id"]),
{:ok, %Token{} = token} <- Token.get_preeexisting_by_app_and_user(app, user),
{:ok, %Authorization{} = auth} <-
Authorization.get_preeexisting_by_app_and_user(app, user) do
conn
|> assign(:token, token)
|> after_create_authorization(auth, %{"authorization" => params})
else
_ -> do_authorize(conn, params)
end
end
defp maybe_reuse_token(conn, params, _user), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
user =
with %{assigns: %{user: %User{} = user}} <- conn do
user
else
_ -> nil
end
scopes =
if scopes == [] do
available_scopes
else
scopes
end
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
user: user,
app: app && Map.delete(app, :client_secret),
response_type: params["response_type"],
client_id: params["client_id"],
available_scopes: available_scopes,
scopes: scopes,
redirect_uri: params["redirect_uri"],
state: params["state"],
params: params,
view_module: OAuthView
})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{"redirect_uri" => @oob_token_redirect_uri}
) do
render(conn, "oob_token_exists.html", %{token: token})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{} = params
) do
app = Repo.preload(token, :app).app
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
default_redirect_uri(app)
end
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
url_params = Maps.put_if_present(url_params, :state, params["state"])
url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
def create_authorization(_, _, opts \\ [])
def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
create_authorization(conn, params, user: user)
end
def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
conn
|> AuthHelper.put_session_user(user.id)
|> after_create_authorization(auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
end
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do
# Enforcing the view to reuse the template when calling from other controllers
conn
|> put_view(OAuthView)
|> render("oob_authorization_created.html", %{auth: auth, view_module: OAuthView})
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
app = Repo.preload(auth, :app).app
# An extra safety measure before we redirect (also done in `do_create_authorization/2`)
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:error, scopes_issue},
%{"authorization" => _} = params
)
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
conn
|> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
|> put_status(:unauthorized)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :confirmation_pending},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:mfa_required, user, auth, _},
params
) do
{:ok, token} = MFA.Token.create(user, auth)
data = %{
"mfa_token" => token.token,
"redirect_uri" => params["authorization"]["redirect_uri"],
"state" => params["authorization"]["state"]
}
MFAController.show(conn, data)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :password_reset_pending},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Password reset is required"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :deactivated},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end
@doc "Renew access_token with refresh_token"
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
after_token_exchange(conn, %{user: user, token: token})
else
_error -> render_invalid_credentials_error(conn)
end
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.get_or_exchange_token(auth, app, user) do
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password"} = params
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
{:ok, app} <- Token.Utils.fetch_app(conn),
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
after_token_exchange(conn, %{token: token})
else
_error ->
handle_token_exchange_error(conn, :invalid_credentails)
end
end
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
|> AuthHelper.put_session_token(token.token)
|> AuthHelper.put_session_user(token.user_id)
|> json(OAuthView.render("token.json", view_params))
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
conn
|> put_status(:forbidden)
|> json(build_and_response_mfa_token(user, auth))
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
render_error(
conn,
:forbidden,
"Your account is currently disabled",
%{},
"account_is_disabled"
)
end
defp handle_token_exchange_error(
%Plug.Conn{} = conn,
{:account_status, :password_reset_pending}
) do
render_error(
conn,
:forbidden,
"Password reset is required",
%{},
"password_reset_required"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
render_error(
conn,
:forbidden,
"Your login is missing a confirmed e-mail address",
%{},
"missing_confirmed_email"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
render_error(
conn,
:forbidden,
"Your account is awaiting approval.",
%{},
"awaiting_approval"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn)
end
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
{:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
conn =
with session_token = AuthHelper.get_session_token(conn),
%Token{token: ^session_token} <- oauth_token do
AuthHelper.delete_session_token(conn)
else
_ -> conn
end
json(conn, %{})
else
_error ->
# RFC 7009: invalid tokens [in the request] do not cause an error response
json(conn, %{})
end
end
def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
# Response for bad request
defp bad_request(%Plug.Conn{} = conn, _) do
render_error(conn, :internal_server_error, "Bad request")
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(%Plug.Conn{} = conn, %{
"provider" => provider,
"authorization" => auth_attrs
}) do
scope =
auth_attrs
|> Scopes.fetch_scopes([])
|> Scopes.to_string()
state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
|> Jason.encode!()
params =
auth_attrs
|> Map.drop(~w(scope scopes client_id redirect_uri))
|> Map.put("state", state)
# Handing the request to Ueberauth
redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params))
end
def request(%Plug.Conn{} = conn, params) do
message =
if params["provider"] do
dgettext("errors", "Unsupported OAuth provider: %{provider}.",
provider: params["provider"]
)
else
dgettext("errors", "Bad OAuth request.")
end
conn
|> put_flash(:error, message)
|> redirect(to: "/")
end
def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
params = callback_params(params)
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
conn
|> put_flash(
:error,
dgettext("errors", "Failed to authenticate: %{message}.", message: message)
)
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
def callback(%Plug.Conn{} = conn, params) do
params = callback_params(params)
with {:ok, registration} <- Authenticator.get_registration(conn) do
auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
case Repo.get_assoc(registration, :user) do
{:ok, user} ->
create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
_ ->
registration_params =
Map.merge(auth_attrs, %{
"nickname" => Registration.nickname(registration),
"email" => Registration.email(registration)
})
conn
|> put_session_registration_id(registration.id)
|> registration_details(%{"authorization" => registration_params})
end
else
error ->
Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
conn
|> put_flash(:error, dgettext("errors", "Failed to set up user account."))
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
end
defp callback_params(%{"state" => state} = params) do
Map.merge(params, Jason.decode!(state))
end
def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
scopes: Scopes.fetch_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth, _user}} <-
{:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
|> put_session_registration_id(nil)
|> after_create_authorization(auth, params)
else
{:create_authorization, error} ->
{:register, handle_create_authorization_error(conn, error, params)}
_ ->
{:register, :generic_error}
end
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{:ok, user} <- Authenticator.create_from_registration(conn, registration) do
conn
|> put_session_registration_id(nil)
|> create_authorization(
params,
user: user
)
else
{:error, changeset} ->
message =
- Enum.map(changeset.errors, fn {field, {error, _}} ->
+ Enum.map_join(changeset.errors, "; ", fn {field, {error, _}} ->
"#{field} #{error}"
end)
- |> Enum.join("; ")
message =
String.replace(
message,
"ap_id has already been taken",
"nickname has already been taken"
)
conn
|> put_status(:forbidden)
|> put_flash(:error, "Error: #{message}.")
|> registration_details(params)
_ ->
{:register, :generic_error}
end
end
defp do_create_authorization(conn, auth_attrs, user \\ nil)
defp do_create_authorization(
%Plug.Conn{} = conn,
%{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_attrs
},
user
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
{:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
{:ok, auth, user}
end
end
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
when is_list(requested_scopes) do
with {:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, requested_scopes),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
{:ok, auth}
end
end
# Note: intended to be a private function but opened for AccountController that logs in on signup
@doc "If checks pass, creates authorization and token for given user, app and requested scopes."
def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
{:ok, token}
end
end
# Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: Routes.auth_url(conn, :login)
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create(user, auth) do
MFAView.render("mfa_response.json", %{token: token, user: user})
end
end
@spec validate_scopes(App.t(), map() | list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(%App{} = app, params) when is_map(params) do
requested_scopes = Scopes.fetch_scopes(params, app.scopes)
validate_scopes(app, requested_scopes)
end
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
Scopes.validate(requested_scopes, app.scopes)
end
def default_redirect_uri(%App{} = app) do
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
defp render_invalid_credentials_error(conn) do
render_error(conn, :bad_request, "Invalid credentials")
end
end
diff --git a/lib/pleroma/web/plugs/parsers/multipart.ex b/lib/pleroma/web/plugs/parsers/multipart.ex
new file mode 100644
index 000000000..6b5ab6af4
--- /dev/null
+++ b/lib/pleroma/web/plugs/parsers/multipart.ex
@@ -0,0 +1,21 @@
+defmodule Pleroma.Web.Plugs.Parsers.Multipart do
+ @multipart Plug.Parsers.MULTIPART
+
+ alias Pleroma.Config
+
+ def init(opts) do
+ opts
+ end
+
+ def parse(conn, "multipart", subtype, headers, opts) do
+ length = Config.get([:instance, :upload_limit])
+
+ opts = @multipart.init([length: length] ++ opts)
+
+ @multipart.parse(conn, "multipart", subtype, headers, opts)
+ end
+
+ def parse(conn, _type, _subtype, _headers, _opts) do
+ {:next, conn}
+ end
+end
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
index 4d7daca56..d992dea63 100644
--- a/lib/pleroma/web/plugs/remote_ip.ex
+++ b/lib/pleroma/web/plugs/remote_ip.ex
@@ -1,48 +1,50 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RemoteIp do
@moduledoc """
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
"""
alias Pleroma.Config
- import Plug.Conn
@behaviour Plug
def init(_), do: nil
def call(%{remote_ip: original_remote_ip} = conn, _) do
if Config.get([__MODULE__, :enabled]) do
- %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
- assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
+ {headers, proxies} = remote_ip_opts()
+ new_remote_ip = RemoteIp.from(conn.req_headers, headers: headers, proxies: proxies)
+
+ if new_remote_ip != original_remote_ip do
+ Map.put(conn, :remote_ip, new_remote_ip)
+ else
+ conn
+ end
else
conn
end
end
defp remote_ip_opts do
- headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
+ headers = Config.get([__MODULE__, :headers], [])
reserved = Config.get([__MODULE__, :reserved], [])
proxies =
Config.get([__MODULE__, :proxies], [])
|> Enum.concat(reserved)
|> Enum.map(&maybe_add_cidr/1)
{headers, proxies}
end
defp maybe_add_cidr(proxy) when is_binary(proxy) do
- proxy =
- cond do
- "/" in String.codepoints(proxy) -> proxy
- InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32"
- InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
- end
-
- InetCidr.parse(proxy, true)
+ cond do
+ "/" in String.codepoints(proxy) -> proxy
+ InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32"
+ InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
+ end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index f2ea679a0..e790b1cdb 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,927 +1,925 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
# Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
# Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
pipeline :app_api do
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
end
pipeline :admin_api do
plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :require_privileged_staff do
plug(Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug)
end
pipeline :require_admin do
plug(Pleroma.Web.Plugs.UserIsAdminPlug)
end
pipeline :mastodon_html do
plug(:browser)
plug(:authenticate)
plug(:after_auth)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
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
pipeline :static_fe do
plug(:fetch_session)
plug(:authenticate)
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
scope "/api/v1/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)
post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
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
)
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
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("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
get("/announcements", AnnouncementController, :index)
post("/announcements", AnnouncementController, :create)
get("/announcements/:id", AnnouncementController, :show)
patch("/announcements/:id", AnnouncementController, :change)
delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_privileged_staff])
delete("/users", UserController, :delete)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/statuses", StatusController, :index)
end
# AdminAPI: admins and mods (staff) can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)
get("/users", UserController, :index)
get("/users/:nickname", UserController, :show)
get("/instances/:instance/statuses", InstanceController, :list_statuses)
delete("/instances/:instance", InstanceController, :delete)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
get("/statuses/:id", StatusController, :show)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:admin_api)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
delete("/", EmojiPackController, :delete)
end
scope "/pack" do
pipe_through(:api)
get("/", EmojiPackController, :show)
end
# Modifying packs
scope "/packs" do
pipe_through(:admin_api)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
get("/", EmojiPackController, :index)
get("/archive", EmojiPackController, :archive)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/main/ostatus", UtilController, :show_subscribe_form)
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("/move_account", UtilController, :move_account)
put("/aliases", UtilController, :add_alias)
get("/aliases", UtilController, :list_aliases)
delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/mutes_import", UserImportController, :mutes)
post("/blocks_import", UserImportController, :blocks)
post("/follow_import", UserImportController, :follow)
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
end
scope "/oauth", Pleroma.Web.OAuth do
# Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
get("/registration_details", OAuthController, :registration_details)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
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("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
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("/federation_status", InstancesController, :show)
end
scope "/api/v1", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
end
scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
pipe_through(:authenticated_api)
get("/translation/languages", TranslationController, :languages)
get("/frontend_settings/:frontend_name", FrontendSettingsController, :list_profiles)
get(
"/frontend_settings/:frontend_name/:profile_name",
FrontendSettingsController,
:get_profile
)
put(
"/frontend_settings/:frontend_name/:profile_name",
FrontendSettingsController,
:update_profile
)
delete(
"/frontend_settings/:frontend_name/:profile_name",
FrontendSettingsController,
:delete_profile
)
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)
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
delete("/conversations/:id", ConversationController, :delete)
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)
get("/media/:id", MediaController, :show)
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)
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)
put("/statuses/:id", StatusController, :update)
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)
get("/statuses/:id/translations/:language", StatusController, :translate)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
get("/timelines/bubble", TimelineController, :bubble)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
get("/tags/:id", TagController, :show)
post("/tags/:id/follow", TagController, :follow)
post("/tags/:id/unfollow", TagController, :unfollow)
end
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
# Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:app_api)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/accounts/lookup", AccountController, :lookup)
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)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
get("/statuses/:id/source", StatusController, :show_source)
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)
get("/directory", DirectoryController, :index)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
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)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, :static_fe])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Notice compatibility routes for other frontends
get("/@:nickname/:id", OStatus.OStatusController, :notice)
get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
get("/:nickname/status/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
pipe_through([:accepts_html_xml, :static_fe])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
end
scope "/", Pleroma.Web.StaticFE do
# Profile pages for static-fe
get("/users/:nickname/with_replies", StaticFEController, :show)
get("/users/:nickname/media", StaticFEController, :show)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
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
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)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
end
scope "/", Pleroma.Web.ActivityPub do
# Note: html format is supported only if static FE is enabled
pipe_through([:accepts_html_json, :static_fe, :activitypub_client])
# 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("/manifest.json", ManifestController, :show)
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)
get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web do
get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxy.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
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/phoenix/live_dashboard")
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 "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
- match(:*, "/api/pleroma*path", LegacyPleromaApiRerouterPlug, [])
- get("/api*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
# TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
def get_api_routes do
__MODULE__.__routes__()
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index a0c3e5c52..f2b571fff 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,365 +1,368 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
plug(
Pleroma.Web.ApiSpec.CastAndValidate
when action != :remote_subscribe and action != :show_subscribe_form
)
plug(
Pleroma.Web.Plugs.FederatingPlug
when action == :remote_subscribe
when action == :show_subscribe_form
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
when action in [
:change_email,
:change_password,
:delete_account,
:update_notificaton_settings,
:disable_account,
:move_account,
:add_alias,
:delete_alias
]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [
:list_aliases
]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation
def show_subscribe_form(conn, %{"nickname" => nick}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - user not found",
"Could not find user"
)
})
end
end
def show_subscribe_form(conn, %{"status_id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity),
%User{} = user <- User.get_cached_by_ap_id(activity.actor),
avatar = User.avatar_url(user) do
conn
|> render("status_interact.html", %{
status_link: ap_id,
status_id: id,
nickname: user.nickname,
avatar: avatar,
error: false
})
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - status not found",
"Could not find status"
)
})
end
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
show_subscribe_form(conn, %{"nickname" => nick})
end
def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
show_subscribe_form(conn, %{"status_id" => id})
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - unknown error",
"Something went wrong."
)
})
end
end
- def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
+ def remote_interaction(
+ %Plug.Conn{body_params: %{ap_id: ap_id, profile: profile}} = conn,
+ _params
+ ) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
else
_e -> json(conn, %{error: "Couldn't find user"})
end
end
defp get_ap_id(activity) do
object = Pleroma.Object.normalize(activity, fetch: false)
case object do
%{data: %{"id" => ap_id}} -> {:ok, ap_id}
_ -> {:no_ap_id, nil}
end
end
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")
end
def emoji(conn, _params) do
emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
Map.put(acc, code, %{image_url: file, tags: tags})
end)
json(conn, emoji)
end
def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
with {:ok, _} <- User.update_notification_settings(user, params) do
json(conn, %{status: "success"})
end
end
def change_password(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <-
User.reset_password(user, %{
password: body_params.new_password,
password_confirmation: body_params.new_password_confirmation
}) do
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
{:error, msg} ->
json(conn, %{error: msg})
end
end
def change_email(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <- User.change_email(user, body_params.email) do
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "Email #{error}."})
_ ->
json(conn, %{error: "Unable to change email."})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
# This endpoint can accept a query param or JSON body for backwards-compatibility.
# Submitting a JSON body is recommended, so passwords don't end up in server logs.
password = body_params[:password] || params[:password] || ""
case CommonAPI.Utils.confirm_current_password(user, password) do
{:ok, user} ->
User.delete(user)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def disable_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params[:password]) do
{:ok, user} ->
User.set_activation_async(user, false)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
{:ok, _user} <- ActivityPub.move(user, target_user) do
json(conn, %{status: "success"})
else
{:not_found, _} ->
conn
|> put_status(404)
|> json(%{error: "Target account not found."})
{:error, error} ->
json(conn, %{error: error})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.add_alias(alias_user) do
json(conn, %{status: "success"})
else
{:not_found, _} ->
conn
|> put_status(404)
|> json(%{error: "Target account does not exist."})
{:error, error} ->
json(conn, %{error: error})
end
end
def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.delete_alias(alias_user) do
json(conn, %{status: "success"})
else
{:error, :no_such_alias} ->
conn
|> put_status(404)
|> json(%{error: "Account has no such alias."})
{:error, error} ->
json(conn, %{error: error})
end
end
def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
alias_nicks =
user
|> User.alias_users()
|> Enum.map(&User.full_nickname/1)
json(conn, %{aliases: alias_nicks})
end
defp find_user_by_nickname(nickname) do
user = User.get_cached_by_nickname(nickname)
if user == nil do
{:not_found, nil}
else
{:ok, user}
end
end
defp find_or_fetch_user_by_nickname(nickname) do
user = User.get_by_nickname(nickname)
if user != nil and user.local do
{:ok, user}
else
with {:ok, user} <- User.fetch_by_nickname(nickname) do
{:ok, user}
else
_ ->
{:not_found, nil}
end
end
end
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
def healthcheck(conn, _params) do
with true <- Config.get([:instance, :healthcheck]),
%{healthy: true} = info <- Healthcheck.system_info() do
json(conn, info)
else
%{healthy: false} = info ->
service_unavailable(conn, info)
_ ->
service_unavailable(conn, %{})
end
end
defp service_unavailable(conn, info) do
conn
|> put_status(:service_unavailable)
|> json(info)
end
end
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
index f5a46ce25..364d0e0e6 100644
--- a/lib/pleroma/web/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -1,225 +1,226 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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.Endpoint
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
require Logger
def host_meta do
base_url = Endpoint.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 =
if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/
else
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
end
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
defp gather_aliases(%User{} = user) do
[user.ap_id | user.also_known_as]
end
def represent_user(user, "JSON") do
%{
"subject" => "acct:#{user.nickname}@#{domain()}",
"aliases" => gather_aliases(user),
"links" => gather_links(user)
}
end
def represent_user(user, "XML") do
aliases =
user
|> gather_aliases()
|> Enum.map(&{:Alias, &1})
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}@#{domain()}"}
] ++ aliases ++ links
}
|> XmlBuilder.to_doc()
end
defp domain do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end
+ @spec webfinger_from_xml(binary()) :: {:ok, map()} | nil
defp webfinger_from_xml(body) do
with {:ok, doc} <- XML.parse_document(body) do
subject = XML.string_from_xpath("//Subject", doc)
subscribe_address =
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
|> XML.string_from_xpath(doc)
ap_id =
~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
|> XML.string_from_xpath(doc)
data = %{
"subject" => subject,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data}
end
end
defp webfinger_from_json(body) do
with {:ok, doc} <- Jason.decode(body) 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"])
{nil, "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
end
def get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd']/@template"
with {:ok, doc} <- 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
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
meta_url = "https://#{domain}/.well-known/host-meta"
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
get_template_from_xml(body)
else
error ->
Logger.warn("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
{:error, :lrdd_not_found}
end
end
defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
case find_lrdd_template(domain) do
{:ok, template} ->
String.replace(template, "{uri}", encoded_account)
_ ->
"https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
end
end
defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
@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
encoded_account = URI.encode("acct:#{account}")
with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
HTTP.get(
address,
[{"accept", "application/xrd+xml,application/jrd+json"}]
) do
case List.keyfind(headers, "content-type", 0) do
{_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do
{:ok, "application", subtype, _} when subtype in ~w(xrd+xml xml) ->
webfinger_from_xml(body)
{:ok, "application", subtype, _} when subtype in ~w(jrd+json json) ->
webfinger_from_json(body)
_ ->
{:error, {:content_type, content_type}}
end
_ ->
{:error, {:content_type, nil}}
end
else
error ->
Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
error
end
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 7944c50ad..f8b3c84a1 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -1,45 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerController do
use Pleroma.Web, :controller
alias Pleroma.Web.WebFinger
plug(Pleroma.Web.Plugs.SetFormatPlug)
plug(Pleroma.Web.Plugs.FederatingPlug)
def host_meta(conn, _params) do
xml = WebFinger.host_meta()
conn
|> put_resp_content_type("application/xrd+xml")
|> send_resp(200, xml)
end
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
when format in ["xml", "xrd+xml"] do
with {:ok, response} <- WebFinger.webfinger(resource, "XML") do
conn
|> put_resp_content_type("application/xrd+xml")
|> send_resp(200, response)
else
_e -> send_resp(conn, 404, "Couldn't find user")
end
end
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
when format in ["json", "jrd+json"] do
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response)
else
_e ->
conn
- |> put_status(404)
+ |> put_status(:not_found)
|> json("Couldn't find user")
end
end
def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request")
end
diff --git a/mix.exs b/mix.exs
index dbfa6263f..00a250002 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,361 +1,360 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
version: version("3.5.0"),
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
- compilers: [:phoenix, :gettext] ++ Mix.compilers(),
+ compilers: [:phoenix] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test],
# Docs
name: "Akkoma",
homepage_url: "https://akkoma.dev/",
source_url: "https://akkoma.dev/AkkomaGang/akkoma",
docs: [
source_url_pattern: "https://akkoma.dev/AkkomaGang/akkoma/blob/develop/%{path}#L%{line}",
logo: "priv/static/images/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
Configuration: Path.wildcard("docs/config/*.md"),
Administration: Path.wildcard("docs/admin/*.md"),
"Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md")
],
main: "readme",
output: "priv/static/doc"
],
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]
]
]
end
def put_otp_version(%{path: target_path} = release) do
File.write!(
Path.join([target_path, "OTP_VERSION"]),
Pleroma.OTPVersion.version()
)
release
end
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
end
def copy_nginx_config(%{path: target_path} = release) do
File.cp!(
"./installation/nginx/akkoma.nginx",
Path.join([target_path, "installation", "akkoma.nginx"])
)
release
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Pleroma.Application, []},
extra_applications: [
:logger,
:runtime_tools,
:comeonin,
:fast_sanitize,
:os_mon,
:ssl
],
included_applications: [:ex_syslogger]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks", "priv/scrubbers"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors, do: System.get_env("CI") == "true"
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
- System.get_env("OAUTH_CONSUMER_STRATEGIES")
+ "OAUTH_CONSUMER_STRATEGIES"
+ |> System.get_env()
|> to_string()
|> String.split()
|> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
dependency
else
[strategy] -> "ueberauth_#{strategy}"
end
end)
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
- {:phoenix, "~> 1.6.11"},
+ {:phoenix, "~> 1.6.15"},
{:tzdata, "~> 1.1.1"},
- {:plug_cowboy, "~> 2.5"},
+ {:plug_cowboy, "~> 2.6"},
{:phoenix_pubsub, "~> 2.1"},
{:phoenix_ecto, "~> 4.4"},
+ {:inet_cidr, "~> 1.0.0"},
{:ecto_enum, "~> 1.4"},
{:ecto_sql, "~> 3.9.0"},
{:postgrex, ">= 0.16.3"},
{:oban, "~> 2.12.1"},
- {:gettext,
- git: "https://github.com/tusooa/gettext.git",
- ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
- override: true},
+ {:gettext, "~> 0.20.0"},
{:bcrypt_elixir, "~> 2.2"},
- {:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
- {:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.1", override: true},
+ {:html_entities, "~> 0.5"},
+ {:phoenix_html, "~> 3.2"},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
- {:poison, "~> 5.0", override: true},
- {:tesla, "~> 1.4.4", override: true},
+ {:tesla, "~> 1.4.4"},
{:castore, "~> 0.1"},
- {:cowlib, "~> 2.9", override: true},
+ {:cowlib, "~> 2.9"},
{:finch, "~> 0.14.0"},
{:jason, "~> 1.2"},
+ {:trailing_format_plug, "~> 0.0.7"},
{:mogrify, "~> 0.9.1"},
{:ex_aws, "~> 2.1.6"},
{:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.7.2"},
{:earmark, "~> 1.4.15"},
{:bbcode_pleroma, "~> 0.2.0"},
{:crypt,
git: "https://github.com/msantos/crypt.git",
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
{:swoosh, "~> 1.0"},
# for gmail adapter in swoosh
{:mail, ">= 0.0.0"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
{:floki, "~> 0.27"},
{:timex, "~> 3.6"},
{:ueberauth, "~> 0.4"},
{:linkify,
git: "https://akkoma.dev/AkkomaGang/linkify.git", branch: "bugfix/line-ending-buffer"},
{:http_signatures, "~> 0.1.1"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
{:recon, "~> 2.5"},
{:joken, "~> 2.0"},
{:benchee, "~> 1.0"},
{:pot, "~> 1.0"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
{:flake_id, "~> 0.1.0"},
{:concurrent_limiter, "~> 0.1.1"},
- {:remote_ip,
- git: "https://git.pleroma.social/pleroma/remote_ip.git",
- ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
+ {:remote_ip, "~> 1.1.0"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:restarter, path: "./restarter"},
{:majic, "~> 1.0"},
{:eblurhash, "~> 1.2.2"},
- {:open_api_spex, "3.10.0"},
+ {:open_api_spex, "~> 3.16.0"},
{:search_parser,
git: "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git",
ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"},
{:nimble_parsec, "~> 1.0", override: true},
- {:phoenix_live_dashboard, "~> 0.6.2"},
+ {:phoenix_live_dashboard, "~> 0.7.2"},
{:ecto_psql_extras, "~> 0.6"},
{:elasticsearch,
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"},
-
- # indirect dependency version override
- {:plug, "~> 1.10.4", override: true},
+ {:poison, ">= 0.0.0"},
## dev & test
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_machina, "~> 2.4", only: :test},
- {:credo, "~> 1.6", only: [:dev, :test], runtime: false},
+ {:credo,
+ git: "https://github.com/rrrene/credo.git",
+ ref: "1c1b99ea41a457761383d81aaf6a606913996fe7",
+ only: [:dev, :test],
+ runtime: false},
{:mock, "~> 0.3.5", only: :test},
{:excoveralls, "0.15.1", only: :test},
{:mox, "~> 1.0", only: :test},
- {:websockex, "~> 0.4.3", only: :test}
+ {:websockex, "~> 0.4.3", only: :test},
+ {:dialyxir, "~> 1.0", only: [:dev], runtime: false}
] ++ oauth_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.migrate": ["pleroma.ecto.migrate"],
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"],
docs: ["pleroma.docs", "docs"],
analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"],
copyright: &add_copyright/1,
"copyright.bump": &bump_copyright/1
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * branch name
# * build metadata:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
dotgit_present? = File.exists?(".git")
git_pre_release =
if git_available? and dotgit_present? do
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
{describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
{commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
# Pre-release version, denoted from patch version with a hyphen
cond do
tag_err == 0 and describe_err == 0 ->
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
commit_hash_err == 0 ->
"0-g" <> String.trim(commit_hash)
true ->
nil
end
end
# Branch name as pre-release version component, denoted with a dot
branch_name =
with true <- git_available?,
true <- dotgit_present?,
{branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()
|> String.replace(identifier_filter, "-")
branch_name
else
_ -> ""
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
env_override = System.get_env("PLEROMA_BUILD_ENV")
env_name =
case env_override do
nil -> env_name
env_override when env_override in ["", "prod"] -> nil
env_override -> env_override
end
# Pre-release version, denoted by appending a hyphen
# and a series of dot separated identifiers
pre_release =
[git_pre_release, branch_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "-" <> String.replace(string, identifier_filter, "-")
end).()
# Build metadata, denoted with a plus sign
build_metadata =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "+" <> String.replace(string, identifier_filter, "-")
end).()
[version, pre_release, build_metadata]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
defp add_copyright(_) do
year = NaiveDateTime.utc_now().year
template = ~s[\
# Pleroma: A lightweight social networking server
# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-# Akkoma: The cooler pleroma
+# Akkoma: Magically expressive social media
# Copyright © 2022-#{year} Akkoma Authors <https://akkoma.dev/>
# SPDX-License-Identifier: AGPL-3.0-only
] |> String.replace("\n", "\\n")
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\) -exec "
grep = "grep -L '# Copyright © [0-9\-]* Pleroma' {} \\;"
xargs = "xargs -n1 sed -i'' '1s;^;#{template};'"
:os.cmd(String.to_charlist("#{find}#{grep} | #{xargs}"))
end
defp bump_copyright(_) do
year = NaiveDateTime.utc_now().year
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\)"
xargs =
"xargs sed -i'' 's;# Copyright © [0-9\-]* Pleroma.*$;# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>;'"
:os.cmd(String.to_charlist("#{find} | #{xargs}"))
end
end
diff --git a/mix.lock b/mix.lock
index e86a58427..0e3ac3514 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,125 +1,126 @@
%{
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"cachex": {:hex, :cachex, "3.4.0", "868b2959ea4aeb328c6b60ff66c8d5123c083466ad3c33d3d8b5f142e13101fb", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "370123b1ab4fba4d2965fb18f87fd758325709787c8c5fce35b3fe80645ccbe5"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
"castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
- "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
+ "credo": {:git, "https://github.com/rrrene/credo.git", "1c1b99ea41a457761383d81aaf6a606913996fe7", [ref: "1c1b99ea41a457761383d81aaf6a606913996fe7"]},
"crypt": {:git, "https://github.com/msantos/crypt.git", "f75cd55325e33cbea198fb41fe41871392f8fb76", [ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"]},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
- "earmark": {:hex, :earmark, "1.4.33", "2b33a505180583f98bfa17317f03973b52081bdb24a11be05a7f4fa6d64dd8bf", [:mix], [{:earmark_parser, "~> 1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "21b31363d6a0a70802cfbaf2de88355778aa76654298a072bce2e01d1858ae06"},
+ "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
+ "earmark": {:hex, :earmark, "1.4.34", "d7f89d3bbd7567a0bffc465e0a949f8f8dcbe43909c3acf96f4761a302cea10c", [:mix], [{:earmark_parser, "~> 1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "90b106f3dad85b133b10d7d628167c88246123fd1cecb4557d83d21ec9e65504"},
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
"ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"},
"elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
+ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.3.3", "61412e524616ea31d3f31675d8bc4c73f277e367dee0ae8245610446f9b778aa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0044f0b6f9ce925666021eafd630de64c2b3404d79c85245cc7c8a9a32d7f104"},
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"},
"fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
- "gettext": {:git, "https://github.com/tusooa/gettext.git", "72fb2496b6c5280ed911bdc3756890e7f38a4808", [ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808"]},
+ "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.1", "ca7ebc1b61542b163644c8c3b1f0e0f41037d35f2395940d3c6c7deceab41fd8", [:mix], [], "hexpm", "cc3b8a007322cc7b624c0c15eec49ee58ac977254ff529a3c482f681465942a3"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"},
"jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"linkify": {:git, "https://akkoma.dev/AkkomaGang/linkify.git", "2567e2c1073fa371fd26fd66dfa5bc77b6919c16", [branch: "bugfix/line-ending-buffer"]},
"mail": {:hex, :mail, "0.2.3", "2c6bb5f8a5f74845fa50ecd0fb45ea16b164026f285f45104f1c4c078cd616d4", [:mix], [], "hexpm", "932b398fa9c69fdf290d7ff63175826e0f1e24414d5b0763bb00a2acfc6c6bf5"},
"majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "912fba81152d4d572e457fd5427f9875b2bc3dbe", [ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
"mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"},
"mogrify": {:hex, :mogrify, "0.9.2", "b360984adea7dd6a55f18028e6327973c58de7f548fdb86c9859848aa904d5b0", [:mix], [], "hexpm", "c18d10fd70ca20e2585301616c89f6e4f7159d92efc9cc8ee579e00c886f699d"},
"mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
"nimble_options": {:hex, :nimble_options, "0.5.1", "5c166f7669e40333191bea38e3bd3811cc13f459f1e4be49e89128a21b5d8c4d", [:mix], [], "hexpm", "d176cf7baa4fef0ceb301ca3eb8b55bd7de3e45f489c4f8b4f2849f1f114ef3e"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"oban": {:hex, :oban, "2.12.1", "f604d7e6a8be9fda4a9b0f6cebbd633deba569f85dbff70c4d25d99a6f023177", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b1844c2b74e0d788b73e5144b0c9d5674cb775eae29d88a36f3c3b48d42d058"},
- "open_api_spex": {:hex, :open_api_spex, "3.10.0", "94e9521ad525b3fcf6dc77da7c45f87fdac24756d4de588cb0816b413e7c1844", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "2dbb2bde3d2b821f06936e8dfaf3284331186556291946d84eeba3750ac28765"},
+ "open_api_spex": {:hex, :open_api_spex, "3.16.0", "9843af4e87550cd8ac5821b10e4c74f1d51f0d4e3310f824d780614743423b25", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "bb0be24a648b73e8fc8cbda17f514b8486262275e8b33e8b5ae66283df972129"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
- "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.3", "2e3d009422addf8b15c3dccc65ce53baccbe26f7cfd21d264680b5867789a9c1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c8845177a866e017dcb7083365393c8f00ab061b8b6b2bda575891079dce81b2"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.4", "615f8f393135de7e0cbb4bd00ba238b1e0cd324b0d90efbaee613c2f02ca5e5c", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "3971221846232021ab5e3c7489fd62ec5bfd6a2e01cae10a317ccf6fb350571c"},
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
- "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
+ "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
- "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"recon": {:hex, :recon, "2.5.2", "cba53fa8db83ad968c9a652e09c3ed7ddcc4da434f27c3eaa9ca47ffb2b1ff03", [:mix, :rebar3], [], "hexpm", "2c7523c8dee91dff41f6b3d63cba2bd49eb6d2fe5bf1eec0df7f87eb5e230e1c"},
- "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
+ "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
"search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"},
- "swoosh": {:hex, :swoosh, "1.8.2", "af9a22ab2c0d20b266f61acca737fa11a121902de9466a39e91bacdce012101c", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d058ba750eafadb6c09a84a352c14c5d1eeeda6e84945fcc95785b7f3067b7db"},
+ "swoosh": {:hex, :swoosh, "1.8.3", "733357d9a65da19c162171f08d1e42a6259236cf44d02a64711b776afbbbaa78", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c699abbac7a296c205055a7501c5d5261320ea1f08bde2392699a9e899815bc7"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ueberauth": {:hex, :ueberauth, "0.10.3", "4a3bd7ab7b5d93d301d264f0f6858392654ee92171f4437d067d1ae227c051d9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1394f36a6c64e97f2038cf95228e7e52b4cb75417962e30418fbe9902b30e6d3"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"vex": {:hex, :vex, "0.9.0", "613ea5eb3055662e7178b83e25b2df0975f68c3d8bb67c1645f0573e1a78d606", [:mix], [], "hexpm", "c69fff44d5c8aa3f1faee71bba1dcab05dd36364c5a629df8bb11751240c857f"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
diff --git a/test/credo/check/consistency/file_location.ex b/test/credo/check/consistency/file_location.ex
index abc55fffc..a457c59a6 100644
--- a/test/credo/check/consistency/file_location.ex
+++ b/test/credo/check/consistency/file_location.ex
@@ -1,166 +1,170 @@
# Pleroma: A lightweight social networking server
# Originally taken from
# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Credo.Check.Consistency.FileLocation do
@moduledoc false
# credo:disable-for-this-file Credo.Check.Readability.Specs
@checkdoc """
File location should follow the namespace hierarchy of the module it defines.
Examples:
- `lib/my_system.ex` should define the `MySystem` module
- `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module
"""
@explanation [warning: @checkdoc]
@special_namespaces [
"controllers",
"views",
"operations",
"channels"
]
# `use Credo.Check` required that module attributes are already defined, so we need
# to place these attributes
# before use/alias expressions.
# credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout
use Credo.Check, category: :warning, base_priority: :high
alias Credo.Code
def run(source_file, params \\ []) do
case verify(source_file, params) do
:ok ->
[]
{:error, module, expected_file} ->
error(IssueMeta.for(source_file, params), module, expected_file)
end
end
defp verify(source_file, params) do
source_file.filename
|> Path.relative_to_cwd()
|> verify(Code.ast(source_file), params)
end
@doc false
def verify(relative_path, ast, params) do
if verify_path?(relative_path, params),
do: ast |> main_module() |> verify_module(relative_path, params),
else: :ok
end
defp verify_path?(relative_path, params) do
case Path.split(relative_path) do
["lib" | _] -> not exclude?(relative_path, params)
["test", "support" | _] -> false
["test", "test_helper.exs"] -> false
["test" | _] -> not exclude?(relative_path, params)
_ -> false
end
end
defp exclude?(relative_path, params) do
params
|> Keyword.get(:exclude, [])
|> Enum.any?(&String.starts_with?(relative_path, &1))
end
defp main_module(ast) do
{_ast, modules} = Macro.prewalk(ast, [], &traverse/2)
Enum.at(modules, -1)
end
defp traverse({:defmodule, _meta, args}, modules) do
[{:__aliases__, _, name_parts}, _module_body] = args
{args, [Module.concat(name_parts) | modules]}
end
defp traverse(ast, state), do: {ast, state}
# empty file - shouldn't really happen, but we'll let it through
defp verify_module(nil, _relative_path, _params), do: :ok
defp verify_module(main_module, relative_path, params) do
parsed_path = parsed_path(relative_path, params)
expected_file =
expected_file_base(parsed_path.root, main_module) <>
Path.extname(parsed_path.allowed)
cond do
expected_file == parsed_path.allowed ->
:ok
special_namespaces?(parsed_path.allowed) ->
original_path = parsed_path.allowed
namespace =
Enum.find(@special_namespaces, original_path, fn namespace ->
String.contains?(original_path, namespace)
end)
allowed = String.replace(original_path, "/" <> namespace, "")
if expected_file == allowed,
do: :ok,
else: {:error, main_module, expected_file}
true ->
{:error, main_module, expected_file}
end
end
defp special_namespaces?(path), do: String.contains?(path, @special_namespaces)
defp parsed_path(relative_path, params) do
parts = Path.split(relative_path)
allowed =
Keyword.get(params, :ignore_folder_namespace, %{})
|> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end)
|> Stream.map(&Path.split/1)
|> Enum.find(&List.starts_with?(parts, &1))
|> case do
nil ->
relative_path
ignore_parts ->
Stream.drop(ignore_parts, -1)
|> Enum.concat(Stream.drop(parts, length(ignore_parts)))
|> Path.join()
end
%{root: hd(parts), allowed: allowed}
end
defp expected_file_base(root_folder, module) do
{parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1)
relative_path =
if parent_namespace == [],
do: "",
else: parent_namespace |> Module.concat() |> Macro.underscore()
file_name = module_name |> Module.concat() |> Macro.underscore()
Path.join([root_folder, relative_path, file_name])
end
defp error(issue_meta, module, expected_file) do
format_issue(issue_meta,
message:
"Mismatch between file name and main module #{inspect(module)}. " <>
"Expected file path to be #{expected_file}. " <>
"Either move the file or rename the module.",
line_no: 1
)
end
+
+ defp append_issues_and_timings(_issues, exec) do
+ exec
+ end
end
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
index e3ae36b46..59674bbc0 100644
--- a/test/pleroma/signature_test.exs
+++ b/test/pleroma/signature_test.exs
@@ -1,139 +1,139 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SignatureTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Signature
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
@private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----"
@public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n"
@rsa_public_key {
:RSAPublicKey,
24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857,
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
65_537
}
defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\""
defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do
test "it returns key" do
expected_result = {:ok, @rsa_public_key}
user = insert(:user, public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
test "it returns error when not found user" do
assert capture_log(fn ->
assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
{:error, :error}
end) =~ "[error] Could not decode user"
end
test "it returns error if public key is nil" do
user = insert(:user, public_key: nil)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
end
end
describe "refetch_public_key/1" do
test "it returns key" do
ap_id = "https://mastodon.social/users/lambadalambda"
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
test "it returns error when not found user" do
assert capture_log(fn ->
{:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
describe "sign/2" do
test "it returns signature headers" do
user =
insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda",
keys: @private_key
})
assert Signature.sign(
user,
%{
host: "test.test",
"content-length": 100
}
) ==
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
end
test "it returns error" do
user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
assert Signature.sign(
user,
%{host: "test.test", "content-length": 100}
) == {:error, []}
end
end
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
{:ok, "https://example.com/users/1234"}
end
test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it deduces the actor id for gotoSocial" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it calls webfinger for 'acct:' accounts" do
with_mock(Pleroma.Web.WebFinger,
- finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end
+ finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
) do
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
{:ok, "https://gensokyo.2hu/users/raymoo"}
end
end
end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
index 7c001ada3..e423b2311 100644
--- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
@@ -1,915 +1,918 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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 ExUnit.CaptureLog
import Pleroma.Factory
import Swoosh.TestAssertions
alias Pleroma.Activity
alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
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
test "with valid `admin_token` query parameter, skips OAuth scopes check" do
clear_config([:admin_token], "password123")
user = insert(:user)
- conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123")
+ conn =
+ get(build_conn(), "/api/v1/pleroma/admin/users/#{user.nickname}?admin_token=password123")
assert json_response(conn, 200)
end
- test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
+ test "GET /api/v1/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
- url = "/api/pleroma/admin/users/#{user.nickname}"
+ url = "/api/v1/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
- describe "PUT /api/pleroma/admin/users/tag" do
+ describe "PUT /api/v1/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[]=" <>
+ "/api/v1/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 empty_json_response(conn)
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 empty_json_response(conn)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
- describe "DELETE /api/pleroma/admin/users/tag" do
+ describe "DELETE /api/v1/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[]=" <>
+ "/api/v1/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 empty_json_response(conn)
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 empty_json_response(conn)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
- describe "/api/pleroma/admin/users/:nickname/permission_group" do
+ describe "/api/v1/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/")
+ |> get("/api/v1/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")
+ |> post("/api/v1/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", %{
+ |> post("/api/v1/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")
+ |> delete("/api/v1/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", %{
+ |> delete("/api/v1/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
- test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
+ test "/api/v1/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")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/password_reset")
resp = json_response(conn, 200)
assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "PUT disable_mfa" do
test "returns 200 and disable 2fa", %{conn: conn} do
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
}
)
response =
conn
- |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname})
+ |> put("/api/v1/pleroma/admin/users/disable_mfa", %{nickname: user.nickname})
|> json_response(200)
assert response == user.nickname
mfa_settings = refresh_record(user).multi_factor_authentication_settings
refute mfa_settings.enabled
refute mfa_settings.totp.confirmed
end
test "returns 404 if user not found", %{conn: conn} do
response =
conn
- |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
+ |> put("/api/v1/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
|> json_response(404)
assert response == %{"error" => "Not found"}
end
end
- describe "GET /api/pleroma/admin/restart" do
+ describe "GET /api/v1/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) == %{}
+ assert conn |> get("/api/v1/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")
+ |> get("/api/v1/pleroma/admin/need_reboot")
|> json_response(200) == %{"need_reboot" => false}
Restarter.Pleroma.need_reboot()
assert conn
- |> get("/api/pleroma/admin/need_reboot")
+ |> get("/api/v1/pleroma/admin/need_reboot")
|> json_response(200) == %{"need_reboot" => true}
on_exit(fn -> Restarter.Pleroma.refresh() end)
end
- describe "GET /api/pleroma/admin/users/:nickname/statuses" do
+ describe "GET /api/v1/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)
insert(:note_activity, user: user)
insert(:note_activity, user: user)
insert(:note_activity, user: user)
%{user: user}
end
test "renders user's statuses", %{conn: conn, user: user} do
- conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
+ conn = get(conn, "/api/v1/pleroma/admin/users/#{user.nickname}/statuses")
assert %{"total" => 3, "activities" => activities} = json_response(conn, 200)
assert length(activities) == 3
end
test "renders user's statuses with pagination", %{conn: conn, user: user} do
%{"total" => 3, "activities" => [activity1]} =
conn
- |> get("/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=1&page=1")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/statuses?page_size=1&page=1")
|> json_response(200)
%{"total" => 3, "activities" => [activity2]} =
conn
- |> get("/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=1&page=2")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/statuses?page_size=1&page=2")
|> json_response(200)
refute activity1 == activity2
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"})
%{"total" => 4, "activities" => activities} =
conn
- |> get("/api/pleroma/admin/users/#{user.nickname}/statuses")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/statuses")
|> json_response(200)
assert length(activities) == 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"})
%{"total" => 5, "activities" => activities} =
conn
- |> get("/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
|> json_response(200)
assert length(activities) == 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)
assert %{"total" => 0, "activities" => []} ==
conn
- |> get("/api/pleroma/admin/users/#{other_user.nickname}/statuses")
+ |> get("/api/v1/pleroma/admin/users/#{other_user.nickname}/statuses")
|> json_response(200)
assert %{"total" => 1, "activities" => [_]} =
conn
|> get(
- "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true"
+ "/api/v1/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true"
)
|> json_response(200)
end
end
- describe "GET /api/pleroma/admin/moderation_log" do
+ describe "GET /api/v1/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")
+ conn = get(conn, "/api/v1/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")
+ conn1 = get(conn, "/api/v1/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")
+ conn2 = get(conn, "/api/v1/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}"
+ "/api/v1/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}")
+ conn1 = get(conn, "/api/v1/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")
+ conn1 = get(conn, "/api/v1/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
test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated",
%{conn: conn} do
clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated)
user = insert(:user, %{local: false, nickname: "u@peer1.com"})
- conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials")
+ conn = get(conn, "/api/v1/pleroma/admin/users/#{user.nickname}/credentials")
assert json_response(conn, 200)
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")
+ conn = get(conn, "/api/v1/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")
+ |> get("/api/v1/pleroma/admin/users/#{user.nickname}/credentials")
assert json_response(conn, :forbidden)
end
end
describe "PATCH /users/:nickname/credentials" do
setup do
user = insert(:user)
[user: user]
end
test "changes password and email", %{conn: conn, admin: admin, user: user} do
assert user.password_reset_pending == false
conn =
- patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
+ patch(conn, "/api/v1/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", %{user: user} do
conn =
build_conn()
|> assign(:user, user)
- |> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{
+ |> patch("/api/v1/pleroma/admin/users/#{user.nickname}/credentials", %{
"password" => "new_password",
"email" => "new_email@example.com",
"name" => "new_name"
})
assert json_response(conn, :forbidden)
end
test "changes actor type from permitted list", %{conn: conn, user: user} do
assert user.actor_type == "Person"
- assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
+ assert patch(conn, "/api/v1/pleroma/admin/users/#{user.nickname}/credentials", %{
"actor_type" => "Service"
})
|> json_response(200) == %{"status" => "success"}
updated_user = User.get_by_id(user.id)
assert updated_user.actor_type == "Service"
- assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
+ assert patch(conn, "/api/v1/pleroma/admin/users/#{user.nickname}/credentials", %{
"actor_type" => "Application"
})
|> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}}
end
test "update non existing user", %{conn: conn} do
- assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{
+ assert patch(conn, "/api/v1/pleroma/admin/users/non-existing/credentials", %{
"password" => "new_password"
})
|> json_response(404) == %{"error" => "Not found"}
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]})
+ patch(conn, "/api/v1/pleroma/admin/users/force_password_reset", %{
+ nicknames: [user.nickname]
+ })
assert empty_json_response(conn) == ""
ObanHelpers.perform_all()
assert User.get_by_id(user.id).password_reset_pending == true
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, is_confirmed: false)
refute first_user.is_confirmed
refute second_user.is_confirmed
ret_conn =
- patch(conn, "/api/pleroma/admin/users/confirm_email", %{
+ patch(conn, "/api/v1/pleroma/admin/users/confirm_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert ret_conn.status == 200
first_user = User.get_by_id(first_user.id)
second_user = User.get_by_id(second_user.id)
assert first_user.is_confirmed
assert second_user.is_confirmed
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, is_confirmed: false)
ret_conn =
- patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{
+ patch(conn, "/api/v1/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}"
ObanHelpers.perform_all()
Pleroma.Emails.UserEmail.account_confirmation_email(first_user)
|> assert_email_sent()
end
end
- describe "/api/pleroma/admin/stats" do
+ describe "/api/v1/pleroma/admin/stats" do
test "status visibility count", %{conn: conn} do
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
- |> get("/api/pleroma/admin/stats")
+ |> get("/api/v1/pleroma/admin/stats")
|> json_response(200)
assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} =
response["status_visibility"]
end
test "by instance", %{conn: conn} do
user1 = insert(:user)
instance2 = "instance2.tld"
user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"})
CommonAPI.post(user1, %{visibility: "public", status: "hey"})
CommonAPI.post(user2, %{visibility: "unlisted", status: "hey"})
CommonAPI.post(user2, %{visibility: "private", status: "hey"})
response =
conn
- |> get("/api/pleroma/admin/stats", instance: instance2)
+ |> get("/api/v1/pleroma/admin/stats", instance: instance2)
|> json_response(200)
assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} =
response["status_visibility"]
end
end
- describe "/api/pleroma/backups" do
+ describe "/api/v1/pleroma/backups" do
test "it creates a backup", %{conn: conn} do
admin = %{id: admin_id, nickname: admin_nickname} = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
user = %{id: user_id, nickname: user_nickname} = insert(:user)
assert "" ==
conn
|> assign(:user, admin)
|> assign(:token, token)
- |> post("/api/pleroma/admin/backups", %{nickname: user.nickname})
+ |> post("/api/v1/pleroma/admin/backups", %{nickname: user.nickname})
|> json_response(200)
assert [backup] = Repo.all(Pleroma.User.Backup)
ObanHelpers.perform_all()
email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup, admin.id)
assert String.contains?(email.html_body, "Admin @#{admin.nickname} requested a full backup")
assert_email_sent(to: {user.name, user.email}, html_body: email.html_body)
log_message = "@#{admin_nickname} requested account backup for @#{user_nickname}"
assert [
%{
data: %{
"action" => "create_backup",
"actor" => %{
"id" => ^admin_id,
"nickname" => ^admin_nickname
},
"message" => ^log_message,
"subject" => %{
"id" => ^user_id,
"nickname" => ^user_nickname
}
}
}
] = Pleroma.ModerationLog |> Repo.all()
end
test "it doesn't limit admins", %{conn: conn} do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
user = insert(:user)
assert "" ==
conn
|> assign(:user, admin)
|> assign(:token, token)
- |> post("/api/pleroma/admin/backups", %{nickname: user.nickname})
+ |> post("/api/v1/pleroma/admin/backups", %{nickname: user.nickname})
|> json_response(200)
assert [_backup] = Repo.all(Pleroma.User.Backup)
assert "" ==
conn
|> assign(:user, admin)
|> assign(:token, token)
- |> post("/api/pleroma/admin/backups", %{nickname: user.nickname})
+ |> post("/api/v1/pleroma/admin/backups", %{nickname: user.nickname})
|> json_response(200)
assert Repo.aggregate(Pleroma.User.Backup, :count) == 2
end
end
end
# Needed for testing
defmodule Pleroma.Web.Endpoint.NotReal do
end
defmodule Pleroma.Captcha.NotReal do
end
diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
index 200682ba9..c84624aff 100644
--- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
@@ -1,155 +1,155 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Config
@dir "test/frontend_static_test"
setup do
clear_config([:instance, :static_dir], @dir)
File.mkdir_p!(Pleroma.Frontend.dir())
on_exit(fn ->
File.rm_rf(@dir)
end)
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 "GET /api/pleroma/admin/frontends" do
+ describe "GET /api/v1/pleroma/admin/frontends" do
test "it lists available frontends", %{conn: conn} do
response =
conn
- |> get("/api/pleroma/admin/frontends")
+ |> get("/api/v1/pleroma/admin/frontends")
|> json_response_and_validate_schema(:ok)
assert Enum.map(response, & &1["name"]) ==
Enum.map(Config.get([:frontends, :available]), fn {_, map} -> map["name"] end)
refute Enum.any?(response, fn frontend -> frontend["installed"] == true end)
end
test "it lists available frontends when no frontend folder was created yet", %{conn: conn} do
File.rm_rf(@dir)
response =
conn
- |> get("/api/pleroma/admin/frontends")
+ |> get("/api/v1/pleroma/admin/frontends")
|> json_response_and_validate_schema(:ok)
assert Enum.map(response, & &1["name"]) ==
Enum.map(Config.get([:frontends, :available]), fn {_, map} -> map["name"] end)
refute Enum.any?(response, fn frontend -> frontend["installed"] == true end)
end
end
- describe "POST /api/pleroma/admin/frontends/install" do
+ describe "POST /api/v1/pleroma/admin/frontends/install" do
test "from available frontends", %{conn: conn} do
clear_config([:frontends, :available], %{
"pleroma" => %{
"ref" => "fantasy",
"name" => "pleroma",
"build_url" => "http://gensokyo.2hu/builds/${ref}"
}
})
Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")}
end)
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/frontends/install", %{name: "pleroma"})
+ |> post("/api/v1/pleroma/admin/frontends/install", %{name: "pleroma"})
|> json_response_and_validate_schema(:ok)
assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"]))
response =
conn
- |> get("/api/pleroma/admin/frontends")
+ |> get("/api/v1/pleroma/admin/frontends")
|> json_response_and_validate_schema(:ok)
assert response == [
%{
"build_url" => "http://gensokyo.2hu/builds/${ref}",
"git" => nil,
"installed" => true,
"name" => "pleroma",
"ref" => "fantasy"
}
]
end
test "from a file", %{conn: conn} do
clear_config([:frontends, :available], %{
"pleroma" => %{
"ref" => "fantasy",
"name" => "pleroma",
"build_dir" => ""
}
})
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/frontends/install", %{
+ |> post("/api/v1/pleroma/admin/frontends/install", %{
name: "pleroma",
file: "test/fixtures/tesla_mock/frontend.zip"
})
|> json_response_and_validate_schema(:ok)
assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"]))
end
test "from an URL", %{conn: conn} do
Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")}
end)
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/frontends/install", %{
+ |> post("/api/v1/pleroma/admin/frontends/install", %{
name: "unknown",
ref: "baka",
build_url: "http://gensokyo.2hu/madeup.zip",
build_dir: ""
})
|> json_response_and_validate_schema(:ok)
assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"]))
end
test "failing returns an error", %{conn: conn} do
Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} ->
%Tesla.Env{status: 404, body: ""}
end)
result =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/frontends/install", %{
+ |> post("/api/v1/pleroma/admin/frontends/install", %{
name: "unknown",
ref: "baka",
build_url: "http://gensokyo.2hu/madeup.zip",
build_dir: ""
})
|> json_response_and_validate_schema(400)
assert result == %{"error" => "Could not download or unzip the frontend"}
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
index c78307fc8..08920a541 100644
--- a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
@@ -1,80 +1,82 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
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
test "GET /instances/:instance/statuses", %{conn: conn} do
user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme")
user2 = insert(:user, local: false, ap_id: "https://test.com/users/test")
insert_pair(:note_activity, user: user)
activity = insert(:note_activity, user: user2)
%{"total" => 2, "activities" => activities} =
- conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
+ conn |> get("/api/v1/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
assert length(activities) == 2
%{"total" => 1, "activities" => [_]} =
- conn |> get("/api/pleroma/admin/instances/test.com/statuses") |> json_response(200)
+ conn |> get("/api/v1/pleroma/admin/instances/test.com/statuses") |> json_response(200)
%{"total" => 0, "activities" => []} =
- conn |> get("/api/pleroma/admin/instances/nonexistent.com/statuses") |> json_response(200)
+ conn
+ |> get("/api/v1/pleroma/admin/instances/nonexistent.com/statuses")
+ |> json_response(200)
CommonAPI.repeat(activity.id, user)
%{"total" => 2, "activities" => activities} =
- conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
+ conn |> get("/api/v1/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
assert length(activities) == 2
%{"total" => 3, "activities" => activities} =
conn
- |> get("/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
+ |> get("/api/v1/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
|> json_response(200)
assert length(activities) == 3
end
test "DELETE /instances/:instance", %{conn: conn} do
user = insert(:user, nickname: "lain@lain.com")
post = insert(:note_activity, user: user)
response =
conn
- |> delete("/api/pleroma/admin/instances/lain.com")
+ |> delete("/api/v1/pleroma/admin/instances/lain.com")
|> json_response(200)
[:ok] = ObanHelpers.perform_all()
assert response == "lain.com"
refute Repo.reload(user).is_active
refute Repo.reload(post)
end
end
diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
index 4d388a1e2..97deb5f82 100644
--- a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
@@ -1,105 +1,105 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
@dir "test/tmp/instance_static"
@default_instance_panel ~s(<p>Welcome to <a href="https://akkoma.dev" target="_blank">Akkoma</a>!</p>)
setup do
File.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
setup do: clear_config([:instance, :static_dir], @dir)
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 "GET /api/pleroma/admin/instance_document/:name" do
+ describe "GET /api/v1/pleroma/admin/instance_document/:name" do
test "return the instance document url", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel")
+ conn = get(conn, "/api/v1/pleroma/admin/instance_document/instance-panel")
assert content = html_response(conn, 200)
assert String.contains?(content, @default_instance_panel)
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)
- |> get("/api/pleroma/admin/instance_document/instance-panel")
+ |> get("/api/v1/pleroma/admin/instance_document/instance-panel")
assert json_response(conn, :forbidden)
end
test "it returns 404 if the instance document with the given name doesn't exist", %{
conn: conn
} do
- conn = get(conn, "/api/pleroma/admin/instance_document/1234")
+ conn = get(conn, "/api/v1/pleroma/admin/instance_document/1234")
assert json_response_and_validate_schema(conn, 404)
end
end
- describe "PATCH /api/pleroma/admin/instance_document/:name" do
+ describe "PATCH /api/v1/pleroma/admin/instance_document/:name" do
test "uploads the instance document", %{conn: conn} do
image = %Plug.Upload{
content_type: "text/html",
path: Path.absname("test/fixtures/custom_instance_panel.html"),
filename: "custom_instance_panel.html"
}
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/admin/instance_document/instance-panel", %{
+ |> patch("/api/v1/pleroma/admin/instance_document/instance-panel", %{
"file" => image
})
assert %{"url" => url} = json_response_and_validate_schema(conn, 200)
index = get(build_conn(), url)
assert html_response(index, 200) == "<h2>Custom instance panel</h2>"
end
end
- describe "DELETE /api/pleroma/admin/instance_document/:name" do
+ describe "DELETE /api/v1/pleroma/admin/instance_document/:name" do
test "deletes the instance document", %{conn: conn} do
File.mkdir!(@dir <> "/instance/")
File.write!(@dir <> "/instance/panel.html", "Custom instance panel")
conn_resp =
conn
- |> get("/api/pleroma/admin/instance_document/instance-panel")
+ |> get("/api/v1/pleroma/admin/instance_document/instance-panel")
assert html_response(conn_resp, 200) == "Custom instance panel"
conn
- |> delete("/api/pleroma/admin/instance_document/instance-panel")
+ |> delete("/api/v1/pleroma/admin/instance_document/instance-panel")
|> json_response_and_validate_schema(200)
conn_resp =
conn
- |> get("/api/pleroma/admin/instance_document/instance-panel")
+ |> get("/api/v1/pleroma/admin/instance_document/instance-panel")
assert content = html_response(conn_resp, 200)
assert String.contains?(content, @default_instance_panel)
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
index 6366061c8..d384c3580 100644
--- a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs
@@ -1,280 +1,280 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InviteControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.UserInviteToken
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 "POST /api/pleroma/admin/users/email_invite, with valid config" do
+ describe "POST /api/v1/pleroma/admin/users/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 =
conn
|> put_req_header("content-type", "application/json;charset=utf-8")
- |> post("/api/pleroma/admin/users/email_invite", %{
+ |> post("/api/v1/pleroma/admin/users/email_invite", %{
email: recipient_email,
name: recipient_name
})
assert json_response_and_validate_schema(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)
|> put_req_header("content-type", "application/json;charset=utf-8")
- |> post("/api/pleroma/admin/users/email_invite", %{
+ |> post("/api/v1/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})
+ |> post("/api/v1/pleroma/admin/users/email_invite", %{email: recipient_email})
|> json_response_and_validate_schema(: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
+ describe "POST /api/v1/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
clear_config([:instance, :registrations_open], false)
clear_config([:instance, :invites_enabled], false)
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/email_invite", %{
+ |> post("/api/v1/pleroma/admin/users/email_invite", %{
email: "foo@bar.com",
name: "JD"
})
assert json_response_and_validate_schema(conn, :bad_request) ==
%{
"error" =>
"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
clear_config([:instance, :registrations_open], true)
clear_config([:instance, :invites_enabled], true)
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/email_invite", %{
+ |> post("/api/v1/pleroma/admin/users/email_invite", %{
email: "foo@bar.com",
name: "JD"
})
assert json_response_and_validate_schema(conn, :bad_request) ==
%{
"error" =>
"To send invites you need to set the `registrations_open` option to false."
}
end
end
- describe "POST /api/pleroma/admin/users/invite_token" do
+ describe "POST /api/v1/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/invite_token")
+ |> post("/api/v1/pleroma/admin/users/invite_token")
invite_json = json_response_and_validate_schema(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 =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/invite_token", %{
+ |> post("/api/v1/pleroma/admin/users/invite_token", %{
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response_and_validate_schema(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 =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
+ |> post("/api/v1/pleroma/admin/users/invite_token", %{"max_use" => 150})
invite_json = json_response_and_validate_schema(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 =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/invite_token", %{
+ |> post("/api/v1/pleroma/admin/users/invite_token", %{
"max_use" => 150,
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response_and_validate_schema(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
+ describe "GET /api/v1/pleroma/admin/users/invites" do
test "no invites", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/users/invites")
+ conn = get(conn, "/api/v1/pleroma/admin/users/invites")
assert json_response_and_validate_schema(conn, 200) == %{"invites" => []}
end
test "with invite", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
- conn = get(conn, "/api/pleroma/admin/users/invites")
+ conn = get(conn, "/api/v1/pleroma/admin/users/invites")
assert json_response_and_validate_schema(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
+ describe "POST /api/v1/pleroma/admin/users/revoke_invite" do
test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
+ |> post("/api/v1/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response_and_validate_schema(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 =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
+ |> post("/api/v1/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"}
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs
index 1818c8a8e..30e941869 100644
--- a/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs
@@ -1,167 +1,167 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Mock
alias Pleroma.Web.MediaProxy
setup do: clear_config([:media_proxy])
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
clear_config([:media_proxy, :enabled], true)
clear_config([:media_proxy, :invalidation, :enabled], true)
clear_config([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script)
{:ok, %{admin: admin, token: token, conn: conn}}
end
- describe "GET /api/pleroma/admin/media_proxy_caches" do
+ describe "GET /api/v1/pleroma/admin/media_proxy_caches" do
test "shows banned MediaProxy URLs", %{conn: conn} do
MediaProxy.put_in_banned_urls([
"http://localhost:4001/media/a688346.jpg",
"http://localhost:4001/media/fb1f4d.jpg"
])
MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg")
MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg")
MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg")
response =
conn
- |> get("/api/pleroma/admin/media_proxy_caches?page_size=2")
+ |> get("/api/v1/pleroma/admin/media_proxy_caches?page_size=2")
|> json_response_and_validate_schema(200)
assert response["page_size"] == 2
assert response["count"] == 5
results = response["urls"]
response =
conn
- |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2")
+ |> get("/api/v1/pleroma/admin/media_proxy_caches?page_size=2&page=2")
|> json_response_and_validate_schema(200)
assert response["page_size"] == 2
assert response["count"] == 5
results = results ++ response["urls"]
response =
conn
- |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3")
+ |> get("/api/v1/pleroma/admin/media_proxy_caches?page_size=2&page=3")
|> json_response_and_validate_schema(200)
results = results ++ response["urls"]
assert results |> Enum.sort() ==
[
"http://localhost:4001/media/wb1f46.jpg",
"http://localhost:4001/media/gb1f44.jpg",
"http://localhost:4001/media/tb13f47.jpg",
"http://localhost:4001/media/fb1f4d.jpg",
"http://localhost:4001/media/a688346.jpg"
]
|> Enum.sort()
end
test "search banned MediaProxy URLs", %{conn: conn} do
MediaProxy.put_in_banned_urls([
"http://localhost:4001/media/a688346.jpg",
"http://localhost:4001/media/ff44b1f4d.jpg"
])
MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg")
MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg")
MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg")
response =
conn
- |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&query=F44")
+ |> get("/api/v1/pleroma/admin/media_proxy_caches?page_size=2&query=F44")
|> json_response_and_validate_schema(200)
assert response["urls"] |> Enum.sort() == [
"http://localhost:4001/media/ff44b1f4d.jpg",
"http://localhost:4001/media/gb1f44.jpg"
]
assert response["page_size"] == 2
assert response["count"] == 2
end
end
- describe "POST /api/pleroma/admin/media_proxy_caches/delete" do
+ describe "POST /api/v1/pleroma/admin/media_proxy_caches/delete" do
test "deleted MediaProxy URLs from banned", %{conn: conn} do
MediaProxy.put_in_banned_urls([
"http://localhost:4001/media/a688346.jpg",
"http://localhost:4001/media/fb1f4d.jpg"
])
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/media_proxy_caches/delete", %{
+ |> post("/api/v1/pleroma/admin/media_proxy_caches/delete", %{
urls: ["http://localhost:4001/media/a688346.jpg"]
})
|> json_response_and_validate_schema(200)
refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg")
assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg")
end
end
- describe "POST /api/pleroma/admin/media_proxy_caches/purge" do
+ describe "POST /api/v1/pleroma/admin/media_proxy_caches/purge" do
test "perform invalidates cache of MediaProxy", %{conn: conn} do
urls = [
"http://example.com/media/a688346.jpg",
"http://example.com/media/fb1f4d.jpg"
]
with_mocks [
{MediaProxy.Invalidation.Script, [],
[
purge: fn _, _ -> {"ok", 0} end
]}
] do
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false})
+ |> post("/api/v1/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false})
|> json_response_and_validate_schema(200)
refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg")
refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg")
end
end
test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do
urls = [
"http://example.com/media/a688346.jpg",
"http://example.com/media/fb1f4d.jpg"
]
with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do
conn
|> put_req_header("content-type", "application/json")
|> post(
- "/api/pleroma/admin/media_proxy_caches/purge",
+ "/api/v1/pleroma/admin/media_proxy_caches/purge",
%{urls: urls, ban: true}
)
|> json_response_and_validate_schema(200)
assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg")
assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg")
end
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs
index d9b25719a..62f156993 100644
--- a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs
@@ -1,219 +1,219 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do
use Pleroma.Web.ConnCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Web.Endpoint
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 "POST /api/pleroma/admin/oauth_app" do
+ describe "POST /api/v1/pleroma/admin/oauth_app" do
test "errors", %{conn: conn} do
response =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/oauth_app", %{})
+ |> post("/api/v1/pleroma/admin/oauth_app", %{})
|> json_response_and_validate_schema(400)
assert %{
"error" => "Missing field: name. Missing field: redirect_uris."
} = response
end
test "success", %{conn: conn} do
base_url = Endpoint.url()
app_name = "Trusted app"
response =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/oauth_app", %{
+ |> post("/api/v1/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url
})
|> json_response_and_validate_schema(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 = Endpoint.url()
app_name = "Trusted app"
response =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/oauth_app", %{
+ |> post("/api/v1/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url,
trusted: true
})
|> json_response_and_validate_schema(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
+ describe "GET /api/v1/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")
+ |> get("/api/v1/pleroma/admin/oauth_app")
|> json_response_and_validate_schema(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=#{page_size}")
+ |> get("/api/v1/pleroma/admin/oauth_app?page_size=#{page_size}")
|> json_response_and_validate_schema(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}")
+ |> get("/api/v1/pleroma/admin/oauth_app?name=#{app.client_name}")
|> json_response_and_validate_schema(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}")
+ |> get("/api/v1/pleroma/admin/oauth_app?client_id=#{app.client_id}")
|> json_response_and_validate_schema(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")
+ |> get("/api/v1/pleroma/admin/oauth_app?trusted=true")
|> json_response_and_validate_schema(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
+ describe "DELETE /api/v1/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))
+ |> delete("/api/v1/pleroma/admin/oauth_app/" <> to_string(app.id))
|> json_response_and_validate_schema(:no_content)
assert response == ""
end
test "with non existance id", %{conn: conn} do
response =
conn
- |> delete("/api/pleroma/admin/oauth_app/0")
+ |> delete("/api/v1/pleroma/admin/oauth_app/0")
|> json_response_and_validate_schema(:bad_request)
assert response == ""
end
end
- describe "PATCH /api/pleroma/admin/oauth_app/:id" do
+ describe "PATCH /api/v1/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
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/oauth_app/#{id}", %{
+ |> patch("/api/v1/pleroma/admin/oauth_app/#{id}", %{
name: name,
trusted: true,
redirect_uris: url,
scopes: scopes,
website: website
})
|> json_response_and_validate_schema(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
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/oauth_app/0")
+ |> patch("/api/v1/pleroma/admin/oauth_app/0")
|> json_response_and_validate_schema(:bad_request)
assert response == ""
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/relay_controller_test.exs b/test/pleroma/web/admin_api/controllers/relay_controller_test.exs
index 11a480cc0..c86339dbd 100644
--- a/test/pleroma/web/admin_api/controllers/relay_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/relay_controller_test.exs
@@ -1,98 +1,98 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.RelayControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.User
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 "relays" do
test "POST /relay", %{conn: conn, admin: admin} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/relay", %{
+ |> post("/api/v1/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response_and_validate_schema(conn, 200) == %{
"actor" => "http://mastodon.example.org/users/admin",
"followed_back" => false
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/relay")
assert json_response_and_validate_schema(conn, 200)["relays"] |> Enum.sort() == [
%{
"actor" => "http://mastodon.example.org/users/admin",
"followed_back" => true
},
%{"actor" => "https://mstdn.io/users/mayuutann", "followed_back" => true}
]
end
test "DELETE /relay", %{conn: conn, admin: admin} do
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/relay", %{
+ |> post("/api/v1/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
conn =
conn
|> put_req_header("content-type", "application/json")
- |> delete("/api/pleroma/admin/relay", %{
+ |> delete("/api/v1/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response_and_validate_schema(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
end
diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs
index 2d526527b..dd859f34f 100644
--- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs
@@ -1,387 +1,387 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Web.CommonAPI
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 "GET /api/pleroma/admin/reports/:id" do
+ describe "GET /api/v1/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]
})
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+ |> post("/api/v1/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is an admin note"
})
response =
conn
- |> get("/api/pleroma/admin/reports/#{report_id}")
+ |> get("/api/v1/pleroma/admin/reports/#{report_id}")
|> json_response_and_validate_schema(:ok)
assert response["id"] == report_id
[notes] = response["notes"]
assert notes["content"] == "this is an admin note"
end
test "returns 404 when report id is invalid", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/reports/test")
+ conn = get(conn, "/api/v1/pleroma/admin/reports/test")
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"}
end
end
- describe "PATCH /api/pleroma/admin/reports" do
+ describe "PATCH /api/v1/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)
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response_and_validate_schema(403)
assert response == %{
"error" => "Insufficient permissions: admin:write:reports."
}
conn
|> assign(:token, write_token)
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response_and_validate_schema(:no_content)
end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
conn
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id}
]
})
|> json_response_and_validate_schema(:no_content)
activity = Activity.get_by_id_with_user_actor(id)
assert activity.data["state"] == "resolved"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} (on user @#{activity.user_actor.nickname}) with 'resolved' state"
end
test "closes report", %{conn: conn, id: id, admin: admin} do
conn
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => id}
]
})
|> json_response_and_validate_schema(:no_content)
activity = Activity.get_by_id_with_user_actor(id)
assert activity.data["state"] == "closed"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} (on user @#{activity.user_actor.nickname}) with 'closed' state"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})
assert "Unsupported state" =
hd(json_response_and_validate_schema(conn, :bad_request))["error"]
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})
assert hd(json_response_and_validate_schema(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
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/reports", %{
+ |> patch("/api/v1/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response_and_validate_schema(:no_content)
activity = Activity.get_by_id_with_user_actor(id)
second_activity = Activity.get_by_id_with_user_actor(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} (on user @#{activity.user_actor.nickname}) with 'resolved' state"
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} (on user @#{second_activity.user_actor.nickname}) with 'closed' state"
end
end
- describe "GET /api/pleroma/admin/reports" do
+ describe "GET /api/v1/pleroma/admin/reports" do
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
|> get(report_path(conn, :index))
|> json_response_and_validate_schema(: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(report_path(conn, :index))
|> json_response_and_validate_schema(: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(report_path(conn, :index, %{state: "open"}))
|> json_response_and_validate_schema(:ok)
assert [open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
assert response["total"] == 1
response =
conn
|> get(report_path(conn, :index, %{state: "closed"}))
|> json_response_and_validate_schema(:ok)
assert [closed_report] = response["reports"]
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
assert response["total"] == 1
assert %{"total" => 0, "reports" => []} ==
conn
|> get(report_path(conn, :index, %{state: "resolved"}))
|> json_response_and_validate_schema(:ok)
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")
+ |> get("/api/v1/pleroma/admin/reports")
assert json_response(conn, :forbidden) ==
%{"error" => "User is not a staff member."}
end
test "returns 403 when requested by anonymous" do
- conn = get(build_conn(), "/api/pleroma/admin/reports")
+ conn = get(build_conn(), "/api/v1/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{
"error" => "Invalid credentials."
}
end
end
- describe "POST /api/pleroma/admin/reports/:id/notes" do
+ describe "POST /api/v1/pleroma/admin/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]
})
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+ |> post("/api/v1/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting!"
})
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
+ |> post("/api/v1/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
assert [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")
+ conn = get(conn, "/api/v1/pleroma/admin/reports")
response = json_response_and_validate_schema(conn, 200)
notes = hd(response["reports"])["notes"]
assert 2 == Enum.count(notes)
note =
notes
|> Enum.find(fn note -> note["content"] == "this is disgusting!" end)
refute is_nil(note)
assert note["user"]["nickname"] == admin.nickname
# We use '=~' because the order of the notes isn't guaranteed
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
assert [note, _] = Repo.all(ReportNote)
- delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
+ delete(conn, "/api/v1/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
assert ReportNote |> Repo.all() |> length() == 1
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/status_controller_test.exs b/test/pleroma/web/admin_api/controllers/status_controller_test.exs
index 3fdf23ba2..370a4953f 100644
--- a/test/pleroma/web/admin_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/status_controller_test.exs
@@ -1,201 +1,201 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
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 "GET /api/pleroma/admin/statuses/:id" do
+ describe "GET /api/v1/pleroma/admin/statuses/:id" do
test "not found", %{conn: conn} do
assert conn
- |> get("/api/pleroma/admin/statuses/not_found")
+ |> get("/api/v1/pleroma/admin/statuses/not_found")
|> json_response_and_validate_schema(:not_found)
end
test "shows activity", %{conn: conn} do
activity = insert(:note_activity)
response =
conn
- |> get("/api/pleroma/admin/statuses/#{activity.id}")
+ |> get("/api/v1/pleroma/admin/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response["id"] == activity.id
account = response["account"]
actor = User.get_by_ap_id(activity.actor)
assert account["id"] == actor.id
assert account["nickname"] == actor.nickname
assert account["is_active"] == actor.is_active
assert account["is_confirmed"] == actor.is_confirmed
end
end
- describe "PUT /api/pleroma/admin/statuses/:id" do
+ describe "PUT /api/v1/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_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response_and_validate_schema(: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_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|> json_response_and_validate_schema(:ok)
refute response["sensitive"]
end
test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "public"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{visibility: "public"})
|> json_response_and_validate_schema(: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_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "private"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{visibility: "private"})
|> json_response_and_validate_schema(:ok)
assert response["visibility"] == "private"
response =
conn
|> put_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"})
|> json_response_and_validate_schema(:ok)
assert response["visibility"] == "unlisted"
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "test"})
+ |> put("/api/v1/pleroma/admin/statuses/#{id}", %{visibility: "test"})
assert %{"error" => "test - Invalid value for enum."} =
json_response_and_validate_schema(conn, :bad_request)
end
end
- describe "DELETE /api/pleroma/admin/statuses/:id" do
+ describe "DELETE /api/v1/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}")
+ |> delete("/api/v1/pleroma/admin/statuses/#{id}")
|> json_response_and_validate_schema(: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")
+ conn = delete(conn, "/api/v1/pleroma/admin/statuses/test")
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"}
end
end
- describe "GET /api/pleroma/admin/statuses" do
+ describe "GET /api/v1/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")
+ |> get("/api/v1/pleroma/admin/statuses")
|> json_response_and_validate_schema(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")
+ |> get("/api/v1/pleroma/admin/statuses?local_only=true")
|> json_response_and_validate_schema(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")
+ conn = get(conn, "/api/v1/pleroma/admin/statuses?godmode=true")
assert json_response_and_validate_schema(conn, 200) |> length() == 3
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
index b199fa704..68876078d 100644
--- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
@@ -1,967 +1,971 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.UserControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
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
test "with valid `admin_token` query parameter, skips OAuth scopes check" do
clear_config([:admin_token], "password123")
user = insert(:user)
- conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123")
+ conn =
+ get(build_conn(), "/api/v1/pleroma/admin/users/#{user.nickname}?admin_token=password123")
assert json_response_and_validate_schema(conn, 200)
end
- test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
+ test "GET /api/v1/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
- url = "/api/pleroma/admin/users/#{user.nickname}"
+ url = "/api/v1/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_and_validate_schema(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_and_validate_schema(conn, :forbidden)
end
end
- describe "DELETE /api/pleroma/admin/users" do
+ describe "DELETE /api/v1/pleroma/admin/users" do
test "single user", %{admin: admin, conn: conn} do
clear_config([:instance, :federating], true)
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]},
banner: %{"url" => [%{"href" => "https://somebanner"}]},
bio: "Hello world!",
name: "A guy"
)
# Create some activities to check they got deleted later
follower = insert(:user)
{:ok, _} = CommonAPI.post(user, %{status: "test"})
{:ok, _, _, _} = CommonAPI.follow(user, follower)
{:ok, _, _, _} = CommonAPI.follow(follower, user)
user = Repo.get(User, user.id)
assert user.note_count == 1
assert user.follower_count == 1
assert user.following_count == 1
assert user.is_active
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end,
perform: fn _, _ -> nil end do
conn =
conn
|> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
+ |> delete("/api/v1/pleroma/admin/users?nickname=#{user.nickname}")
ObanHelpers.perform_all()
refute User.get_by_nickname(user.nickname).is_active
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response_and_validate_schema(conn, 200) == [user.nickname]
user = Repo.get(User, user.id)
refute user.is_active
assert user.avatar == %{}
assert user.banner == %{}
assert user.note_count == 0
assert user.follower_count == 0
assert user.following_count == 0
assert user.bio == ""
assert user.name == nil
assert called(Pleroma.Web.Federator.publish(:_))
end
end
test "multiple users", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
response =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
- |> delete("/api/pleroma/admin/users", %{
+ |> delete("/api/v1/pleroma/admin/users", %{
nicknames: [user_one.nickname, user_two.nickname]
})
|> json_response_and_validate_schema(200)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
assert response -- [user_one.nickname, user_two.nickname] == []
end
end
- describe "/api/pleroma/admin/users" do
+ describe "/api/v1/pleroma/admin/users" do
test "Create", %{conn: conn} do
response =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users", %{
+ |> post("/api/v1/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
})
|> json_response_and_validate_schema(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")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users", %{
+ |> post("/api/v1/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response_and_validate_schema(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")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users", %{
+ |> post("/api/v1/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response_and_validate_schema(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")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users", %{
+ |> post("/api/v1/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response_and_validate_schema(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
+ describe "/api/v1/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
user = insert(:user)
- conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
+ conn = get(conn, "/api/v1/pleroma/admin/users/#{user.nickname}")
assert user_response(user) == json_response_and_validate_schema(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}")
+ conn = get(conn, "/api/v1/pleroma/admin/users/#{user.nickname}")
assert %{"error" => "Not found"} == json_response_and_validate_schema(conn, 404)
end
end
- describe "/api/pleroma/admin/users/follow" do
+ describe "/api/v1/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")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/follow", %{
+ |> post("/api/v1/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
+ describe "/api/v1/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")
|> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/users/unfollow", %{
+ |> post("/api/v1/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 "GET /api/pleroma/admin/users" do
+ describe "GET /api/v1/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"])
user2 = insert(:user, is_approved: false, registration_reason: "I'm a chill dude")
- conn = get(conn, "/api/pleroma/admin/users?page=1")
+ conn = get(conn, "/api/v1/pleroma/admin/users?page=1")
users = [
user_response(
user2,
%{
"local" => true,
"is_approved" => false,
"registration_reason" => "I'm a chill dude",
"actor_type" => "Person"
}
),
user_response(user, %{"local" => false, "tags" => ["foo", "bar"]}),
user_response(
admin,
%{"roles" => %{"admin" => true, "moderator" => false}}
)
]
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "pagination works correctly with service users", %{conn: conn} do
service1 = User.get_or_create_service_actor_by_ap_id(Endpoint.url() <> "/meido", "meido")
insert_list(25, :user)
assert %{"count" => 26, "page_size" => 10, "users" => users1} =
conn
- |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"})
+ |> get("/api/v1/pleroma/admin/users?page=1&filters=", %{page_size: "10"})
|> json_response_and_validate_schema(200)
assert Enum.count(users1) == 10
assert service1 not in users1
assert %{"count" => 26, "page_size" => 10, "users" => users2} =
conn
- |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"})
+ |> get("/api/v1/pleroma/admin/users?page=2&filters=", %{page_size: "10"})
|> json_response_and_validate_schema(200)
assert Enum.count(users2) == 10
assert service1 not in users2
assert %{"count" => 26, "page_size" => 10, "users" => users3} =
conn
- |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"})
+ |> get("/api/v1/pleroma/admin/users?page=3&filters=", %{page_size: "10"})
|> json_response_and_validate_schema(200)
assert Enum.count(users3) == 6
assert service1 not in users3
end
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
- conn = get(conn, "/api/pleroma/admin/users?page=2")
+ conn = get(conn, "/api/v1/pleroma/admin/users?page=2")
assert json_response_and_validate_schema(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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?query=bo")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user, %{"local" => true})]
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?query=domain.com")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?query=nickname@domain.com")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?name=display")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?email=email@example.com")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
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")
+ conn1 = get(conn, "/api/v1/pleroma/admin/users?query=a&page_size=1&page=1")
assert json_response_and_validate_schema(conn1, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [user_response(user2)]
}
- conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2")
+ conn2 = get(conn, "/api/v1/pleroma/admin/users?query=a&page_size=1&page=2")
assert json_response_and_validate_schema(conn2, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [user_response(user)]
}
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")
+ |> get("/api/v1/pleroma/admin/users?query=bo&filters=local")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?filters=local")
users = [
user_response(user),
user_response(admin, %{
"roles" => %{"admin" => true, "moderator" => false}
}),
user_response(old_admin, %{
"is_active" => true,
"roles" => %{"admin" => true, "moderator" => false}
})
]
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "only unconfirmed users", %{conn: conn} do
sad_user = insert(:user, nickname: "sadboy", is_confirmed: false)
old_user = insert(:user, nickname: "oldboy", is_confirmed: false)
insert(:user, nickname: "happyboy", is_approved: true)
insert(:user, is_confirmed: true)
result =
conn
- |> get("/api/pleroma/admin/users?filters=unconfirmed")
+ |> get("/api/v1/pleroma/admin/users?filters=unconfirmed")
|> json_response_and_validate_schema(200)
users =
Enum.map([old_user, sad_user], fn user ->
user_response(user, %{
"is_confirmed" => false,
"is_approved" => true
})
end)
assert result == %{"count" => 2, "page_size" => 50, "users" => users}
end
test "only unapproved users", %{conn: conn} do
user =
insert(:user,
nickname: "sadboy",
is_approved: false,
registration_reason: "Plz let me in!"
)
insert(:user, nickname: "happyboy", is_approved: true)
- conn = get(conn, "/api/pleroma/admin/users?filters=need_approval")
+ conn = get(conn, "/api/v1/pleroma/admin/users?filters=need_approval")
users = [
user_response(
user,
%{"is_approved" => false, "registration_reason" => "Plz let me in!"}
)
]
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?filters=is_admin")
users = [
user_response(second_admin, %{
"is_active" => true,
"roles" => %{"admin" => true, "moderator" => false}
}),
user_response(admin, %{
"is_active" => true,
"roles" => %{"admin" => true, "moderator" => false}
})
]
assert json_response_and_validate_schema(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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?filters=is_moderator")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
user_response(moderator, %{
"is_active" => true,
"roles" => %{"admin" => false, "moderator" => true}
})
]
}
end
test "load users with actor_type is Person", %{admin: admin, conn: conn} do
insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> get(user_path(conn, :index), %{actor_types: ["Person"]})
|> json_response_and_validate_schema(200)
users = [
user_response(user2),
user_response(user1),
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}})
]
assert response == %{"count" => 3, "page_size" => 50, "users" => users}
end
test "load users with actor_type is Person and Service", %{admin: admin, conn: conn} do
user_service = insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> get(user_path(conn, :index), %{actor_types: ["Person", "Service"]})
|> json_response_and_validate_schema(200)
users = [
user_response(user2),
user_response(user1),
user_response(user_service, %{"actor_type" => "Service"}),
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}})
]
assert response == %{"count" => 4, "page_size" => 50, "users" => users}
end
test "load users with actor_type is Service", %{conn: conn} do
user_service = insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
insert(:user)
insert(:user)
response =
conn
|> get(user_path(conn, :index), %{actor_types: ["Service"]})
|> json_response_and_validate_schema(200)
users = [user_response(user_service, %{"actor_type" => "Service"})]
assert response == %{"count" => 1, "page_size" => 50, "users" => users}
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")
+ conn = get(conn, "/api/v1/pleroma/admin/users?tags[]=first&tags[]=second")
users = [
user_response(user2, %{"tags" => ["second"]}),
user_response(user1, %{"tags" => ["first"]})
]
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "`active` filters out users pending approval", %{token: token} do
insert(:user, is_approved: false)
%{id: user_id} = insert(:user, is_approved: true)
%{id: admin_id} = token.user
conn =
build_conn()
|> assign(:user, token.user)
|> assign(:token, token)
- |> get("/api/pleroma/admin/users?filters=active")
+ |> get("/api/v1/pleroma/admin/users?filters=active")
assert %{
"count" => 2,
"page_size" => 50,
"users" => [
%{"id" => ^user_id},
%{"id" => ^admin_id}
]
} = json_response_and_validate_schema(conn, 200)
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, is_active: false)
insert(:user, nickname: "ken", local: true, is_active: false)
insert(:user, nickname: "bobb", local: false, is_active: true)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
- |> get("/api/pleroma/admin/users?filters=deactivated,external")
+ |> get("/api/v1/pleroma/admin/users?filters=deactivated,external")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "it omits relay user", %{admin: admin, conn: conn} do
assert %User{} = Relay.get_actor()
- conn = get(conn, "/api/pleroma/admin/users")
+ conn = get(conn, "/api/v1/pleroma/admin/users")
assert json_response_and_validate_schema(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}})
]
}
end
end
- test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
+ test "PATCH /api/v1/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
user_one = insert(:user, is_active: false)
user_two = insert(:user, is_active: false)
conn =
conn
|> put_req_header("content-type", "application/json")
|> patch(
- "/api/pleroma/admin/users/activate",
+ "/api/v1/pleroma/admin/users/activate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response_and_validate_schema(conn, 200)
assert Enum.map(response["users"], & &1["is_active"]) == [true, true]
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
+ test "PATCH /api/v1/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
user_one = insert(:user, is_active: true)
user_two = insert(:user, is_active: true)
conn =
conn
|> put_req_header("content-type", "application/json")
|> patch(
- "/api/pleroma/admin/users/deactivate",
+ "/api/v1/pleroma/admin/users/deactivate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response_and_validate_schema(conn, 200)
assert Enum.map(response["users"], & &1["is_active"]) == [false, false]
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/approve", %{admin: admin, conn: conn} do
+ test "PATCH /api/v1/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
user_one = insert(:user, is_approved: false)
user_two = insert(:user, is_approved: false)
conn =
conn
|> put_req_header("content-type", "application/json")
|> patch(
- "/api/pleroma/admin/users/approve",
+ "/api/v1/pleroma/admin/users/approve",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response_and_validate_schema(conn, 200)
assert Enum.map(response["users"], & &1["is_approved"]) == [true, true]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
end
- test "PATCH /api/pleroma/admin/users/suggest", %{admin: admin, conn: conn} do
+ test "PATCH /api/v1/pleroma/admin/users/suggest", %{admin: admin, conn: conn} do
user1 = insert(:user, is_suggested: false)
user2 = insert(:user, is_suggested: false)
response =
conn
|> put_req_header("content-type", "application/json")
|> patch(
- "/api/pleroma/admin/users/suggest",
+ "/api/v1/pleroma/admin/users/suggest",
%{nicknames: [user1.nickname, user2.nickname]}
)
|> json_response_and_validate_schema(200)
assert Enum.map(response["users"], & &1["is_suggested"]) == [true, true]
[user1, user2] = Repo.reload!([user1, user2])
assert user1.is_suggested
assert user2.is_suggested
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} added suggested users: @#{user1.nickname}, @#{user2.nickname}"
end
- test "PATCH /api/pleroma/admin/users/unsuggest", %{admin: admin, conn: conn} do
+ test "PATCH /api/v1/pleroma/admin/users/unsuggest", %{admin: admin, conn: conn} do
user1 = insert(:user, is_suggested: true)
user2 = insert(:user, is_suggested: true)
response =
conn
|> put_req_header("content-type", "application/json")
|> patch(
- "/api/pleroma/admin/users/unsuggest",
+ "/api/v1/pleroma/admin/users/unsuggest",
%{nicknames: [user1.nickname, user2.nickname]}
)
|> json_response_and_validate_schema(200)
assert Enum.map(response["users"], & &1["is_suggested"]) == [false, false]
[user1, user2] = Repo.reload!([user1, user2])
refute user1.is_suggested
refute user2.is_suggested
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} removed suggested users: @#{user1.nickname}, @#{user2.nickname}"
end
- test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
+ test "PATCH /api/v1/pleroma/admin/users/:nickname/toggle_activation", %{
+ admin: admin,
+ conn: conn
+ } do
user = insert(:user)
conn =
conn
|> put_req_header("content-type", "application/json")
- |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
+ |> patch("/api/v1/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response_and_validate_schema(conn, 200) ==
user_response(
user,
%{"is_active" => !user.is_active}
)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
defp user_response(user, attrs \\ %{}) do
%{
"is_active" => user.is_active,
"id" => user.id,
"email" => user.email,
"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),
"is_confirmed" => true,
"is_approved" => true,
"is_suggested" => false,
"url" => user.ap_id,
"registration_reason" => nil,
"actor_type" => "Person",
"created_at" => CommonAPI.Utils.to_masto_date(user.inserted_at)
}
|> Map.merge(attrs)
end
end
diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs
index 512baf813..cf573cd00 100644
--- a/test/pleroma/web/fallback_test.exs
+++ b/test/pleroma/web/fallback_test.exs
@@ -1,94 +1,88 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FallbackTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
describe "neither preloaded data nor metadata attached to" do
test "GET /registration/:token", %{conn: conn} do
response = get(conn, "/registration/foo")
assert html_response(response, 200) =~ "<!--server-generated-meta-->"
end
test "GET /*path", %{conn: conn} do
assert conn
|> get("/foo")
|> html_response(200) =~ "<!--server-generated-meta-->"
end
end
test "GET /*path adds a title", %{conn: conn} do
clear_config([:instance, :name], "a cool title")
assert conn
|> get("/")
|> html_response(200) =~ "<title>a cool title</title>"
end
describe "preloaded data and metadata attached to" do
test "GET /:maybe_nickname_or_id", %{conn: conn} do
clear_config([:instance, :name], "a cool title")
user = insert(:user)
user_missing = get(conn, "/foo")
user_present = get(conn, "/#{user.nickname}")
assert html_response(user_missing, 200) =~ "<!--server-generated-meta-->"
refute html_response(user_present, 200) =~ "<!--server-generated-meta-->"
assert html_response(user_present, 200) =~ "initial-results"
assert html_response(user_present, 200) =~ "<title>a cool title</title>"
end
test "GET /*path", %{conn: conn} do
assert conn
|> get("/foo")
|> html_response(200) =~ "<!--server-generated-meta-->"
refute conn
|> get("/foo/bar")
|> html_response(200) =~ "<!--server-generated-meta-->"
end
end
describe "preloaded data is attached to" do
test "GET /main/public", %{conn: conn} do
clear_config([:instance, :name], "a cool title")
public_page = get(conn, "/main/public")
refute html_response(public_page, 200) =~ "<!--server-generated-meta-->"
assert html_response(public_page, 200) =~ "initial-results"
assert html_response(public_page, 200) =~ "<title>a cool title</title>"
end
test "GET /main/all", %{conn: conn} do
public_page = get(conn, "/main/all")
refute html_response(public_page, 200) =~ "<!--server-generated-meta-->"
assert html_response(public_page, 200) =~ "initial-results"
end
end
- test "GET /api*path", %{conn: conn} do
- assert conn
- |> get("/api/foo")
- |> json_response(404) == %{"error" => "Not implemented"}
- end
-
test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do
assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/"
end
test "OPTIONS /*path", %{conn: conn} do
assert conn
|> options("/foo")
|> response(204) == ""
assert conn
|> options("/foo/bar")
|> response(204) == ""
end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index cda4c9e03..d76db0c0d 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,2383 +1,2383 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ScheduledActivityWorker
import Pleroma.Factory
setup do: clear_config([:instance, :federating])
setup do: clear_config([:instance, :allow_relay])
setup do: clear_config([:rich_media, :enabled])
setup do: clear_config([:mrf, :policies])
setup do: clear_config([:mrf_keyword, :reject])
setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
setup do: clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
describe "posting statuses" do
setup do: oauth_access(["write:statuses"])
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
clear_config([:instance, :federating], true)
Config.get([:instance, :allow_relay], true)
response =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
"status" => "Hello world",
"visibility" => "public"
})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
ObanHelpers.perform_all()
response =
conn
- |> get("api/v1/statuses/#{response["id"]}", %{})
+ |> get("/api/v1/statuses/#{response["id"]}", %{})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
end
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "0"
})
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response_and_validate_schema(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => 0
})
# Idempotency plug response means detection fail
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "False"
})
assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
conn_four =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
assert Activity.get_by_id(fourth_id)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: fourth_id},
scheduled_at: expires_at
)
end
test "automatically setting a post expiry if status_ttl_days is set" do
user = insert(:user, status_ttl_days: 1)
%{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"status" => "aa chikichiki banban"
})
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id_with_object(id)
{:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"])
assert Timex.diff(
expires_at,
DateTime.utc_now(),
:hours
) == 23
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: DateTime.add(DateTime.utc_now(), 1 * 60 * 60 * 24)
)
end
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
# 1 minute
expires_in = 1 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
# 5 minutes
expires_in = 5 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
end
test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{"status" => "GNO/Linux"})
+ |> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
end
test "posting an undefined status with an attachment", %{user: user, conn: conn} do
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
assert json_response_and_validate_schema(conn, 200)
end
test "replying to a status", %{user: user, conn: conn} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{
user: user,
conn: conn
} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "The message visibility must be direct"
}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
real_status = json_response_and_validate_schema(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
conn
|> assign(:user, refresh_record(conn.assigns.user))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
fake_status = json_response_and_validate_schema(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
assert real_status == fake_status
end
test "fake statuses' preview card is not cached", %{conn: conn} do
clear_config([:rich_media, :enabled], true)
Tesla.Mock.mock(fn
%{
method: :get,
url: "https://example.com/twitter-card"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
env ->
apply(HttpRequestMock, :request, [env])
end)
conn1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp",
"preview" => true
})
conn2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/twitter-card",
"preview" => true
})
assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
json_response_and_validate_schema(conn2, 200)
end
test "posting a status with OGP link preview", %{conn: conn} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
clear_config([:rich_media, :enabled], true)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
+ |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
test "discloses application metadata when enabled" do
user = insert(:user, disclose_client: true)
%{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
%Pleroma.Web.OAuth.Token{
app: %Pleroma.Web.OAuth.App{
client_name: app_name,
website: app_website
}
} = token
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe is my copilot"
})
assert %{
"content" => "cofe is my copilot"
} = json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
- |> get("api/v1/statuses/#{activity}")
+ |> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "cofe is my copilot",
"application" => %{
"name" => ^app_name,
"website" => ^app_website
}
} = json_response_and_validate_schema(result, 200)
end
test "hides application metadata when disabled" do
user = insert(:user, disclose_client: false)
%{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "club mate is my wingman"
})
assert %{"content" => "club mate is my wingman"} =
json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
- |> get("api/v1/statuses/#{activity}")
+ |> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "club mate is my wingman",
"application" => nil
} = json_response_and_validate_schema(result, 200)
end
end
describe "posting scheduled statuses" do
setup do: oauth_access(["write:statuses"])
test "creates a scheduled activity", %{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} =
json_response_and_validate_schema(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
test "with expiration" do
%{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at,
"expires_in" => 300
})
|> json_response_and_validate_schema(200)
assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/scheduled_statuses/#{status_id}")
|> json_response_and_validate_schema(200)
end
test "ignores nil values", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})
assert result = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(result["id"])
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"media_attachments" => [media_attachment]} =
json_response_and_validate_schema(conn, 200)
assert %{"type" => "image"} = media_attachment
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
# TODO
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
end
describe "posting polls" do
setup do: oauth_access(["write:statuses"])
test "posting a poll", %{conn: conn} do
time = NaiveDateTime.utc_now()
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{
"options" => ["Rei", "Asuka", "Misato"],
"expires_in" => 420
}
})
response = json_response_and_validate_schema(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
assert response["poll"]["expired"] == false
question = Object.get_by_id(response["poll"]["id"])
# closed contains utc timezone
assert question.data["closed"] =~ "Z"
end
test "option limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
"expires_in" => 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too far in the future"
end
test "scheduled poll", %{conn: conn} do
clear_config([ScheduledActivity, :enabled], true)
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
%{"id" => scheduled_id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "very cool poll",
"poll" => %{
"options" => ~w(a b c),
"expires_in" => 420
},
"scheduled_at" => scheduled_at
})
|> json_response_and_validate_schema(200)
assert {:ok, %{id: activity_id}} =
perform_job(ScheduledActivityWorker, %{
activity_id: scheduled_id
})
refute_enqueued(worker: ScheduledActivityWorker)
object =
Activity
|> Repo.get(activity_id)
|> Object.normalize()
assert object.data["content"] == "very cool poll"
assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3
end
end
test "get a status" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == to_string(activity.id)
end
defp local_and_remote_activities do
local = insert(:note_activity)
remote = insert(:note_activity, local: false)
{:ok, local: local, remote: remote}
end
describe "status with restrict unauthenticated activities for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
test "getting a status that doesn't exist returns 404" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "get a direct status" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
res = json_response_and_validate_schema(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs" do
%{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
assert [%{"id" => ^id1}, %{"id" => ^id2}] =
Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
end
describe "getting statuses by ids with restricted unauthenticated for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert json_response_and_validate_schema(res_conn, 200) == []
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
remote_id = remote.id
assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
local_id = local.id
assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
object = Object.normalize(activity, fetch: false)
content = object.data["content"]
source = object.data["source"]
result =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert match?(%{"content" => ^content, "text" => ^source}, result)
refute Activity.get_by_id(activity.id)
end
test "when it doesn't exist" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
end
test "when you didn't create it" do
%{conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity)
conn = delete(conn, "/api/v1/statuses/#{activity.id}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're an admin or moderator", %{conn: conn} do
activity1 = insert(:note_activity)
activity2 = insert(:note_activity)
admin = insert(:user, is_admin: true)
moderator = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, admin)
|> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity1.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
res_conn =
conn
|> assign(:user, moderator)
|> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity2.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id)
end
end
describe "reblogging" do
setup do: oauth_access(["write:statuses"])
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/statuses/#{activity.id}/reblog",
%{"visibility" => "private"}
)
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, _} = CommonAPI.favorite(user2, activity.id)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
{:ok, _} = CommonAPI.repeat(activity.id, user2)
conn_res =
build_conn()
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
} = json_response_and_validate_schema(conn_res, 200)
conn_res =
build_conn()
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
} = json_response_and_validate_schema(conn_res, 200)
assert to_string(activity.id) == id
end
test "author can reblog own private status", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
setup do: oauth_access(["write:statuses"])
test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error when activity does not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/foo/unreblog")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "favoriting" do
setup do: oauth_access(["write:favourites"])
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "favoriting twice will just return 200", %{conn: conn} do
activity = insert(:note_activity)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "unfavoriting" do
setup do: oauth_access(["write:favourites"])
test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.favorite(user, activity.id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unfavourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
%{activity: activity}
end
setup do: clear_config([:instance, :max_pinned_statuses], 1)
test "pin status", %{conn: conn, user: user, activity: activity} do
id = activity.id
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "non authenticated user", %{activity: activity} do
assert build_conn()
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(403) == %{"error" => "Invalid credentials."}
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "Non-public status cannot be pinned"
}
end
test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(200)
assert [] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response_and_validate_schema(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response_and_validate_schema(400)
end
test "on pin removes deletion job, on unpin reschedule deletion" do
%{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
assert %{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
- |> post("api/v1/statuses", %{
+ |> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/pin")
|> json_response_and_validate_schema(200)
refute_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/unpin")
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
end
end
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"
%{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
author = insert(:user)
{:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
{:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
response2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
bookmarks = get(conn, bookmarks_uri)
assert [
json_response_and_validate_schema(response2, 200),
json_response_and_validate_schema(response1, 200)
] ==
json_response_and_validate_schema(bookmarks, 200)
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
bookmarks = get(conn, bookmarks_uri)
assert [json_response_and_validate_schema(response2, 200)] ==
json_response_and_validate_schema(bookmarks, 200)
end
describe "conversation muting" do
setup do: oauth_access(["write:mutes"])
setup do
post_user = insert(:user)
{:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
%{activity: activity}
end
test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response_and_validate_schema(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
assert json_response_and_validate_schema(conn, 400) == %{
"error" => "conversation is already muted"
}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => false} =
conn
# |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
Object.normalize(replied_to, fetch: false).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response_and_validate_schema(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
- |> get("api/v1/timelines/home")
+ |> get("/api/v1/timelines/home")
[reblogged_activity] = json_response_and_validate_schema(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
build_conn()
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
conn
|> assign(:token, nil)
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
response =
conn
|> get(favourited_by_url)
|> json_response_and_validate_schema(200)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false)
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
conn: conn
} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
{:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(404)
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
response =
build_conn()
|> get("/api/v1/statuses/#{id3}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "context when restrict_unauthenticated is on" do
user = insert(:user)
remote_user = insert(:user, local: false)
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} =
CommonAPI.post(remote_user, %{status: "3", in_reply_to_status_id: id2, local: false})
response =
build_conn()
|> get("/api/v1/statuses/#{id2}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}],
"descendants" => [%{"id" => ^id3}]
} = response
clear_config([:restrict_unauthenticated, :activities, :local], true)
response =
build_conn()
|> get("/api/v1/statuses/#{id2}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [],
"descendants" => []
} = response
end
test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
{:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
{:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
result =
conn
|> get("/api/v1/favourites?limit=1")
assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
assert post_id == second_post.id
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
[_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
assert max_id == third_favorite.id
result =
conn
|> get("/api/v1/favourites?max_id=#{max_id}")
assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
json_response_and_validate_schema(result, 200)
assert first_post_id == first_post.id
assert third_post_id == third_post.id
end
test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
{:ok, last_like} = CommonAPI.favorite(user, activity.id)
first_conn = get(conn, "/api/v1/favourites")
assert [status] = json_response_and_validate_schema(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _} = CommonAPI.favorite(user, second_activity.id)
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
assert [second_status] = json_response_and_validate_schema(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn = get(conn, "/api/v1/favourites?limit=0")
assert [] = json_response_and_validate_schema(third_conn, 200)
end
test "expires_at is nil for another user" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
{:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
assert DateTime.diff(expires_at, a_expires_at) == 0
%{conn: conn} = oauth_access(["read:statuses"])
assert %{"pleroma" => %{"expires_at" => nil}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
end
describe "local-only statuses" do
test "posting a local only status" do
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"visibility" => "local"
})
local = Utils.as_local_public()
assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
json_response_and_validate_schema(conn_one, 200)
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
end
test "other users can read local-only posts" do
user = insert(:user)
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
received =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
assert received["id"] == activity.id
end
test "anonymous users cannot see local-only posts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
_received =
build_conn()
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:not_found)
end
end
describe "muted reactions" do
test "index" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "show" do
# %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
%{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => []
}
} = result
result =
conn
|> get("/api/v1/statuses/#{activity.id}?with_muted=true")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
} = result
end
end
describe "posting quotes" do
setup do: oauth_access(["write:statuses"])
test "posting a quote", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} = CommonAPI.post(user, %{status: "tell me, for whom do you fight?"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Hmph, how very glib",
"quote_id" => quoted_status.id
})
response = json_response_and_validate_schema(conn, 200)
assert response["quote_id"] == quoted_status.id
assert response["quote"]["id"] == quoted_status.id
assert response["quote"]["content"] == quoted_status.object.data["content"]
end
test "posting a quote, quoting a status that isn't public", %{conn: conn} do
user = insert(:user)
Enum.each(["private", "local", "direct"], fn visibility ->
{:ok, quoted_status} =
CommonAPI.post(user, %{
status: "tell me, for whom do you fight?",
visibility: visibility
})
assert %{"error" => "You can only quote public or unlisted statuses"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Hmph, how very glib",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(422)
end)
end
test "posting a quote, after quote, the status gets deleted", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} =
CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(200)
{:ok, _} = CommonAPI.delete(quoted_status.id, user)
resp =
conn
|> get("/api/v1/statuses/#{resp["id"]}")
|> json_response_and_validate_schema(200)
assert is_nil(resp["quote"])
end
test "posting a quote of a deleted status", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} =
CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
{:ok, _} = CommonAPI.delete(quoted_status.id, user)
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(422)
end
test "posting a quote of a status that doesn't exist", %{conn: conn} do
assert %{"error" => "You can't quote a status that doesn't exist"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => "oops"
})
|> json_response_and_validate_schema(422)
end
end
describe "get status history" do
setup do
%{conn: build_conn()}
end
test "unedited post", %{conn: conn} do
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [_] = json_response_and_validate_schema(conn, 200)
end
test "edited post", %{conn: conn} do
note =
insert(
:note,
data: %{
"formerRepresentations" => %{
"type" => "OrderedCollection",
"orderedItems" => [
%{
"type" => "Note",
"content" => "mew mew 2",
"summary" => "title 2"
},
%{
"type" => "Note",
"content" => "mew mew 1",
"summary" => "title 1"
}
],
"totalItems" => 2
}
}
)
activity = insert(:note_activity, note: note)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
json_response_and_validate_schema(conn, 200)
end
end
describe "translating statuses" do
setup do
clear_config([:translator, :enabled], true)
clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
clear_config([:deepl, :api_key], "deepl_api_key")
oauth_access(["read:statuses"])
end
test "listing languages", %{conn: conn} do
Tesla.Mock.mock_global(fn
%{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!([
%{language: "en", name: "English"}
])
}
%{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!([
%{language: "ja", name: "Japanese"}
])
}
end)
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/akkoma/translation/languages")
response = json_response_and_validate_schema(conn, 200)
assert %{
"source" => [%{"code" => "en", "name" => "English"}],
"target" => [%{"code" => "ja", "name" => "Japanese"}]
} = response
end
test "should return text and detected language", %{conn: conn} do
clear_config([:deepl, :tier], :free)
Tesla.Mock.mock_global(fn
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!(%{
translations: [
%{
"text" => "Tell me, for whom do you fight?",
"detected_source_language" => "ja"
}
]
})
}
end)
user = insert(:user)
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
response = json_response_and_validate_schema(conn, 200)
assert response["text"] == "Tell me, for whom do you fight?"
assert response["detected_language"] == "ja"
end
test "should not allow translating of statuses you cannot see", %{conn: conn} do
clear_config([:deepl, :tier], :free)
Tesla.Mock.mock_global(fn
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!(%{
translations: [
%{
"text" => "Tell me, for whom do you fight?",
"detected_source_language" => "ja"
}
]
})
}
end)
user = insert(:user)
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
json_response_and_validate_schema(conn, 404)
end
end
describe "get status source" do
setup do
%{conn: build_conn()}
end
test "it returns the source", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
id = activity.id
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
json_response_and_validate_schema(conn, 200)
end
end
describe "update status" do
setup do
oauth_access(["write:statuses"])
end
test "it updates the status" do
%{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
response =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
end
test "it updates the attachments", %{conn: conn, user: user} do
attachment = insert(:attachment, user: user)
attachment_id = to_string(attachment.id)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "mew mew #abc",
"spoiler_text" => "#def",
"media_ids" => [attachment_id]
})
|> json_response_and_validate_schema(200)
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
end
test "it does not update visibility", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["visibility"] == "private"
end
test "it refuses to update when original post is not by the user", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:forbidden)
end
test "it returns 404 if the user cannot see the post", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:not_found)
end
end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
index f609db7a4..aa9006681 100644
--- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -1,1109 +1,1109 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
alias Pleroma.User
alias Pleroma.Web.CommonAPI
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "home" do
setup do: oauth_access(["read:statuses"])
test "does NOT embed account/pleroma/relationship in statuses", %{
user: user,
conn: conn
} do
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
response =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
assert Enum.all?(response, fn n ->
get_in(n, ["account", "pleroma", "relationship"]) == %{}
end)
end
test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do
{: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"})
conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct")
assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"])
assert public_activity.id in status_ids
assert unlisted_activity.id in status_ids
assert private_activity.id in status_ids
refute direct_activity.id in status_ids
end
test "muted emotions", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home?with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "filtering", %{conn: conn, user: user} do
local_user = insert(:user)
{:ok, user, local_user} = User.follow(user, local_user)
{:ok, local_activity} = CommonAPI.post(local_user, %{status: "Status"})
with_media = create_with_media_activity(local_user)
remote_user = insert(:user, local: false)
{:ok, _user, remote_user} = User.follow(user, remote_user)
remote_activity = create_remote_activity(remote_user)
without_filter_ids =
conn
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in without_filter_ids
assert remote_activity.id in without_filter_ids
assert with_media.id in without_filter_ids
only_local_ids =
conn
|> get("/api/v1/timelines/home?local=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in only_local_ids
refute remote_activity.id in only_local_ids
assert with_media.id in only_local_ids
only_local_media_ids =
conn
|> get("/api/v1/timelines/home?local=true&only_media=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
refute local_activity.id in only_local_media_ids
refute remote_activity.id in only_local_media_ids
assert with_media.id in only_local_media_ids
remote_ids =
conn
|> get("/api/v1/timelines/home?remote=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
refute local_activity.id in remote_ids
assert remote_activity.id in remote_ids
refute with_media.id in remote_ids
assert conn
|> get("/api/v1/timelines/home?remote=true&only_media=true")
|> json_response_and_validate_schema(200) == []
assert conn
|> get("/api/v1/timelines/home?remote=true&local=true")
|> json_response_and_validate_schema(200) == []
end
end
describe "public" do
@tag capture_log: true
test "the public timeline", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
with_media = create_with_media_activity(user)
remote = insert(:note_activity, local: false)
assert conn
|> get("/api/v1/timelines/public?local=False")
|> json_response_and_validate_schema(:ok)
|> length == 3
local_ids =
conn
|> get("/api/v1/timelines/public?local=True")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in local_ids
assert with_media.id in local_ids
refute remote.id in local_ids
local_ids =
conn
|> get("/api/v1/timelines/public?local=True")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in local_ids
assert with_media.id in local_ids
refute remote.id in local_ids
local_ids =
conn
|> get("/api/v1/timelines/public?local=True&only_media=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
refute activity.id in local_ids
assert with_media.id in local_ids
refute remote.id in local_ids
local_ids =
conn
|> get("/api/v1/timelines/public?local=1")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in local_ids
assert with_media.id in local_ids
refute remote.id in local_ids
remote_id = remote.id
assert [%{"id" => ^remote_id}] =
conn
|> get("/api/v1/timelines/public?remote=true")
|> json_response_and_validate_schema(:ok)
with_media_id = with_media.id
assert [%{"id" => ^with_media_id}] =
conn
|> get("/api/v1/timelines/public?only_media=true")
|> json_response_and_validate_schema(:ok)
assert conn
|> get("/api/v1/timelines/public?remote=true&only_media=true")
|> json_response_and_validate_schema(:ok) == []
# does not contain repeats
{:ok, _} = CommonAPI.repeat(activity.id, user)
assert [_, _] =
conn
|> get("/api/v1/timelines/public?local=true")
|> json_response_and_validate_schema(:ok)
end
test "the public timeline includes only public statuses for an authenticated user" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
{:ok, _activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"})
{:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"})
{:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end
test "doesn't return replies if follower is posting with blocked user" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
[blockee, friend] = insert_list(2, :user)
{:ok, blocker, friend} = User.follow(blocker, friend)
{:ok, _} = User.block(blocker, blockee)
conn = assign(conn, :user, blocker)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
# Still shows replies from yourself
{:ok, %{id: reply_from_me}} =
CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee})
response =
get(conn, "/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert length(response) == 2
[%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response
end
test "doesn't return posts from users who blocked you when :blockers_visible is disabled" do
clear_config([:activitypub, :blockers_visible], false)
%{conn: conn, user: blockee} = oauth_access(["read:statuses"])
blocker = insert(:user)
{:ok, _} = User.block(blocker, blockee)
conn = assign(conn, :user, blockee)
{:ok, _} = CommonAPI.post(blocker, %{status: "hey!"})
response =
get(conn, "/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
- assert length(response) == 0
+ assert response == []
end
test "doesn't return replies if follow is posting with users from blocked domain" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
friend = insert(:user)
blockee = insert(:user, ap_id: "https://example.com/users/blocked")
{:ok, blocker, friend} = User.follow(blocker, friend)
{:ok, blocker} = User.block_domain(blocker, "example.com")
conn = assign(conn, :user, blocker)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
res_conn = get(conn, "/api/v1/timelines/public")
activities = json_response_and_validate_schema(res_conn, 200)
[%{"id" => ^activity_id}] = activities
end
test "can be filtered by instance", %{conn: conn} do
user = insert(:user, ap_id: "https://lain.com/users/lain")
insert(:note_activity, local: false)
insert(:note_activity, local: false)
{:ok, _} = CommonAPI.post(user, %{status: "test"})
conn = get(conn, "/api/v1/timelines/public?instance=lain.com")
assert length(json_response_and_validate_schema(conn, :ok)) == 1
end
test "muted emotions", %{conn: conn} do
user = insert(:user)
token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
conn =
conn
|> assign(:user, user)
|> assign(:token, token)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/timelines/public?with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "should return local-only posts for authenticated users" do
user = insert(:user)
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
{:ok, %{id: id}} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
result =
conn
|> get("/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id}] = result
end
test "should not return local-only posts for users without read:statuses" do
user = insert(:user)
%{user: _reader, conn: conn} = oauth_access([])
{:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
result =
conn
|> get("/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert [] = result
end
test "should not return local-only posts for anonymous users" do
user = insert(:user)
{:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
result =
build_conn()
|> get("/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert [] = result
end
end
defp local_and_remote_activities do
insert(:note_activity)
insert(:note_activity, local: false)
:ok
end
describe "public with restrict unauthenticated timeline for local and federated timelines" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
test "if user is unauthenticated", %{conn: conn} do
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "authorization required for timeline view"
}
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "authorization required for timeline view"
}
end
test "if user is authenticated" do
%{conn: conn} = oauth_access(["read:statuses"])
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "public with restrict unauthenticated timeline for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
test "if user is unauthenticated", %{conn: conn} do
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "authorization required for timeline view"
}
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
test "if user is authenticated", %{conn: _conn} do
%{conn: conn} = oauth_access(["read:statuses"])
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "public with restrict unauthenticated timeline for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
test "if user is unauthenticated", %{conn: conn} do
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "authorization required for timeline view"
}
end
test "if user is authenticated", %{conn: _conn} do
%{conn: conn} = oauth_access(["read:statuses"])
res_conn = get(conn, "/api/v1/timelines/public?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/timelines/public?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "direct" do
test "direct timeline", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two, user_one} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "private"
})
conn_user_two =
conn
|> assign(:user, user_two)
|> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
# Only direct should be visible here
- res_conn = get(conn_user_two, "api/v1/timelines/direct")
+ res_conn = get(conn_user_two, "/api/v1/timelines/direct")
assert [status] = json_response_and_validate_schema(res_conn, :ok)
assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"]
# User should be able to see their own direct message
res_conn =
build_conn()
|> assign(:user, user_one)
|> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"]))
- |> get("api/v1/timelines/direct")
+ |> get("/api/v1/timelines/direct")
[status] = json_response_and_validate_schema(res_conn, :ok)
assert %{"visibility" => "direct"} = status
# Both should be visible here
- res_conn = get(conn_user_two, "api/v1/timelines/home")
+ res_conn = get(conn_user_two, "/api/v1/timelines/home")
[_s1, _s2] = json_response_and_validate_schema(res_conn, :ok)
# Test pagination
Enum.each(1..20, fn _ ->
{:ok, _} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "direct"
})
end)
- res_conn = get(conn_user_two, "api/v1/timelines/direct")
+ res_conn = get(conn_user_two, "/api/v1/timelines/direct")
statuses = json_response_and_validate_schema(res_conn, :ok)
assert length(statuses) == 20
max_id = List.last(statuses)["id"]
- res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}")
+ res_conn = get(conn_user_two, "/api/v1/timelines/direct?max_id=#{max_id}")
assert [status] = json_response_and_validate_schema(res_conn, :ok)
assert status["url"] != direct.data["id"]
end
test "doesn't include DMs from blocked users" do
%{user: blocker, conn: conn} = oauth_access(["read:statuses"])
blocked = insert(:user)
other_user = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blocked)
{:ok, _blocked_direct} =
CommonAPI.post(blocked, %{
status: "Hi @#{blocker.nickname}!",
visibility: "direct"
})
{:ok, direct} =
CommonAPI.post(other_user, %{
status: "Hi @#{blocker.nickname}!",
visibility: "direct"
})
- res_conn = get(conn, "api/v1/timelines/direct")
+ res_conn = get(conn, "/api/v1/timelines/direct")
[status] = json_response_and_validate_schema(res_conn, :ok)
assert status["id"] == direct.id
end
end
describe "list" do
setup do: oauth_access(["read:lists"])
test "does not contain retoots", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
{:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."})
{:ok, _} = CommonAPI.repeat(activity_one.id, other_user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn = get(conn, "/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
assert id == to_string(activity_two.id)
end
test "works with pagination", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
Enum.each(1..30, fn i ->
CommonAPI.post(other_user, %{status: "post number #{i}"})
end)
res =
get(conn, "/api/v1/timelines/list/#{list.id}?limit=1")
|> json_response_and_validate_schema(:ok)
assert length(res) == 1
[first] = res
res =
get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30")
|> json_response_and_validate_schema(:ok)
assert length(res) == 29
end
test "list timeline", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
{:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn = get(conn, "/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
assert id == to_string(activity_two.id)
end
test "list timeline does not leak non-public statuses for unfollowed users", %{
user: user,
conn: conn
} do
other_user = insert(:user)
{:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
{:ok, _activity_two} =
CommonAPI.post(other_user, %{
status: "Marisa is cute.",
visibility: "private"
})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn = get(conn, "/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
assert id == to_string(activity_one.id)
end
test "muted emotions", %{user: user, conn: conn} do
user2 = insert(:user)
user3 = insert(:user)
{:ok, activity} = CommonAPI.post(user2, %{status: "."})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user3, "🎅")
User.mute(user, user3)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, user2)
result =
conn
|> get("/api/v1/timelines/list/#{list.id}")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/timelines/list/#{list.id}?with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "filtering", %{user: user, conn: conn} do
{:ok, list} = Pleroma.List.create("name", user)
local_user = insert(:user)
{:ok, local_activity} = CommonAPI.post(local_user, %{status: "Marisa is stupid."})
with_media = create_with_media_activity(local_user)
{:ok, list} = Pleroma.List.follow(list, local_user)
remote_user = insert(:user, local: false)
remote_activity = create_remote_activity(remote_user)
{:ok, list} = Pleroma.List.follow(list, remote_user)
all_ids =
conn
|> get("/api/v1/timelines/list/#{list.id}")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in all_ids
assert with_media.id in all_ids
assert remote_activity.id in all_ids
only_local_ids =
conn
|> get("/api/v1/timelines/list/#{list.id}?local=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in only_local_ids
assert with_media.id in only_local_ids
refute remote_activity.id in only_local_ids
only_local_media_ids =
conn
|> get("/api/v1/timelines/list/#{list.id}?local=true&only_media=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
refute local_activity.id in only_local_media_ids
assert with_media.id in only_local_media_ids
refute remote_activity.id in only_local_media_ids
remote_ids =
conn
|> get("/api/v1/timelines/list/#{list.id}?remote=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
refute local_activity.id in remote_ids
refute with_media.id in remote_ids
assert remote_activity.id in remote_ids
assert conn
|> get("/api/v1/timelines/list/#{list.id}?remote=true&only_media=true")
|> json_response_and_validate_schema(200) == []
only_media_ids =
conn
|> get("/api/v1/timelines/list/#{list.id}?only_media=true")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
refute local_activity.id in only_media_ids
assert with_media.id in only_media_ids
refute remote_activity.id in only_media_ids
assert conn
|> get("/api/v1/timelines/list/#{list.id}?only_media=true&local=true&remote=true")
|> json_response_and_validate_schema(200) == []
end
end
describe "hashtag" do
setup do: oauth_access(["n/a"])
@tag capture_log: true
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
{:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"})
with_media = create_with_media_activity(following)
remote = insert(:user, local: false)
remote_activity = create_remote_activity(remote)
all_ids =
conn
|> get("/api/v1/timelines/tag/2hu")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in all_ids
assert with_media.id in all_ids
assert remote_activity.id in all_ids
# works for different capitalization too
all_ids =
conn
|> get("/api/v1/timelines/tag/2HU")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in all_ids
assert with_media.id in all_ids
assert remote_activity.id in all_ids
local_ids =
conn
|> get("/api/v1/timelines/tag/2hu?local=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
assert activity.id in local_ids
assert with_media.id in local_ids
refute remote_activity.id in local_ids
remote_ids =
conn
|> get("/api/v1/timelines/tag/2hu?remote=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
refute activity.id in remote_ids
refute with_media.id in remote_ids
assert remote_activity.id in remote_ids
media_ids =
conn
|> get("/api/v1/timelines/tag/2hu?only_media=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
refute activity.id in media_ids
assert with_media.id in media_ids
refute remote_activity.id in media_ids
media_local_ids =
conn
|> get("/api/v1/timelines/tag/2hu?only_media=true&local=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
refute activity.id in media_local_ids
assert with_media.id in media_local_ids
refute remote_activity.id in media_local_ids
ids =
conn
|> get("/api/v1/timelines/tag/2hu?only_media=true&local=true&remote=true")
|> json_response_and_validate_schema(:ok)
|> Enum.map(& &1["id"])
refute activity.id in ids
refute with_media.id in ids
refute remote_activity.id in ids
assert conn
|> get("/api/v1/timelines/tag/2hu?only_media=true&remote=true")
|> json_response_and_validate_schema(:ok) == []
end
test "multi-hashtag timeline", %{conn: conn} do
user = insert(:user)
{:ok, activity_test} = CommonAPI.post(user, %{status: "#test"})
{:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"})
{:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"})
any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1")
[status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok)
assert to_string(activity_test.id) == status_test["id"]
assert to_string(activity_test1.id) == status_test1["id"]
assert to_string(activity_none.id) == status_none["id"]
restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none")
assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok)
all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none")
assert [status_none] == json_response_and_validate_schema(all_test, :ok)
end
test "muted emotions", %{conn: conn} do
user = insert(:user)
token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
conn =
conn
|> assign(:user, user)
|> assign(:token, token)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test #2hu"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/timelines/tag/2hu")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/timelines/tag/2hu?with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
end
describe "hashtag timeline handling of :restrict_unauthenticated setting" do
setup do
user = insert(:user)
{:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"})
{:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"})
activity1
|> Ecto.Changeset.change(%{local: false})
|> Pleroma.Repo.update()
base_uri = "/api/v1/timelines/tag/tag1"
error_response = %{"error" => "authorization required for timeline view"}
%{base_uri: base_uri, error_response: error_response}
end
defp ensure_authenticated_access(base_uri) do
%{conn: auth_conn} = oauth_access(["read:statuses"])
res_conn = get(auth_conn, "#{base_uri}?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(auth_conn, "#{base_uri}?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
test "with default settings on private instances, returns 403 for unauthenticated users", %{
conn: conn,
base_uri: base_uri,
error_response: error_response
} do
clear_config([:instance, :public], false)
clear_config([:restrict_unauthenticated, :timelines])
for local <- [true, false] do
res_conn = get(conn, "#{base_uri}?local=#{local}")
assert json_response_and_validate_schema(res_conn, :unauthorized) == error_response
end
ensure_authenticated_access(base_uri)
end
test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{
conn: conn,
base_uri: base_uri,
error_response: error_response
} do
clear_config([:restrict_unauthenticated, :timelines, :local], true)
clear_config([:restrict_unauthenticated, :timelines, :federated], true)
for local <- [true, false] do
res_conn = get(conn, "#{base_uri}?local=#{local}")
assert json_response_and_validate_schema(res_conn, :unauthorized) == error_response
end
ensure_authenticated_access(base_uri)
end
test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline",
%{conn: conn, base_uri: base_uri, error_response: error_response} do
clear_config([:restrict_unauthenticated, :timelines, :local], false)
clear_config([:restrict_unauthenticated, :timelines, :federated], true)
res_conn = get(conn, "#{base_uri}?local=true")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "#{base_uri}?local=false")
assert json_response_and_validate_schema(res_conn, :unauthorized) == error_response
ensure_authenticated_access(base_uri)
end
test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline",
%{conn: conn, base_uri: base_uri, error_response: error_response} do
# (but not to local public activities which are delivered as part of federated timeline)
clear_config([:restrict_unauthenticated, :timelines, :local], true)
clear_config([:restrict_unauthenticated, :timelines, :federated], false)
res_conn = get(conn, "#{base_uri}?local=true")
assert json_response_and_validate_schema(res_conn, :unauthorized) == error_response
# Note: local activities get delivered as part of federated timeline
res_conn = get(conn, "#{base_uri}?local=false")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
ensure_authenticated_access(base_uri)
end
end
describe "bubble" do
setup do: oauth_access(["read:statuses"])
test "filtering", %{conn: conn, user: user} do
clear_config([:instance, :local_bubble], [])
# our endpoint host has a port in it so let's set the AP ID
local_user = insert(:user, %{ap_id: "https://localhost/users/user"})
remote_user = insert(:user, %{ap_id: "https://example.com/users/remote_user"})
{:ok, user, local_user} = User.follow(user, local_user)
{:ok, _user, remote_user} = User.follow(user, remote_user)
{:ok, local_activity} = CommonAPI.post(local_user, %{status: "Status"})
remote_activity = create_remote_activity(remote_user)
# If nothing, only include ours
clear_config([:instance, :local_bubble], [])
one_instance =
conn
|> get("/api/v1/timelines/bubble")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in one_instance
# If we have others, also include theirs
clear_config([:instance, :local_bubble], ["example.com"])
two_instances =
conn
|> get("/api/v1/timelines/bubble")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in two_instances
assert remote_activity.id in two_instances
end
end
defp create_remote_activity(user) do
obj =
insert(:note, %{
data: %{
"to" => [
"https://www.w3.org/ns/activitystreams#Public",
User.ap_followers(user)
]
},
user: user
})
insert(:note_activity, %{
note: obj,
recipients: [
"https://www.w3.org/ns/activitystreams#Public",
User.ap_followers(user)
],
user: user,
local: false
})
end
defp create_with_media_activity(user) do
obj = insert(:attachment_note, user: user)
insert(:note_activity, %{
note: obj,
recipients: ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(user)],
user: user
})
end
end
diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs
index 130cbe8d1..2ba909dad 100644
--- a/test/pleroma/web/mastodon_api/update_credentials_test.exs
+++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs
@@ -1,639 +1,639 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
alias Pleroma.Repo
alias Pleroma.User
use Pleroma.Web.ConnCase
import Mock
import Pleroma.Factory
describe "updating credentials" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "sets user settings in a generic way", %{conn: conn} do
res_conn =
patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
pleroma_fe: %{
theme: "bla"
}
}
})
assert user_data = json_response_and_validate_schema(res_conn, 200)
assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
user = Repo.get(User, user_data["id"])
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "bla"
}
}
})
assert user_data = json_response_and_validate_schema(res_conn, 200)
assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "bla"}
}
user = Repo.get(User, user_data["id"])
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _activity -> :ok end do
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "blub"
}
}
})
assert user_data = json_response_and_validate_schema(res_conn, 200)
assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "blub"}
}
assert_called(Pleroma.Web.Federator.publish(:_))
end
end
test "updates the user's bio", %{conn: conn} do
user2 = insert(:user)
raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.."
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["note"] ==
~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{user2.id}" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..)
assert user_data["source"]["note"] == raw_bio
user = Repo.get(User, user_data["id"])
assert user.raw_bio == raw_bio
end
test "updates the user's locking status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["locked"] == true
end
test "updates the user's allow_following_move", %{user: user, conn: conn} do
assert user.allow_following_move == true
conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
assert refresh_record(user).allow_following_move == false
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["allow_following_move"] == false
end
test "updates the user's default scope", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["privacy"] == "unlisted"
end
test "updates the user's privacy", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{source: %{privacy: "unlisted"}})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["privacy"] == "unlisted"
end
test "updates the user's hide_followers status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_followers"] == true
end
test "updates the user's discoverable status", %{conn: conn} do
assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"})
|> json_response_and_validate_schema(:ok)
assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"})
|> json_response_and_validate_schema(:ok)
end
test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{
hide_followers_count: "true",
hide_follows_count: "true"
})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_followers_count"] == true
assert user_data["pleroma"]["hide_follows_count"] == true
end
test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do
response =
conn
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
|> json_response_and_validate_schema(200)
assert response["pleroma"]["skip_thread_containment"] == true
assert refresh_record(user).skip_thread_containment
end
test "updates the user's hide_follows status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_follows"] == true
end
test "updates the user's hide_favorites status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["hide_favorites"] == true
end
test "updates the user's show_role status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["pleroma"]["show_role"] == false
end
test "updates the user's no_rich_text status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["source"]["pleroma"]["no_rich_text"] == true
end
test "updates the user's name", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["display_name"] == "markorepairs"
update_activity = Repo.one(Pleroma.Activity)
assert update_activity.data["type"] == "Update"
assert update_activity.data["object"]["name"] == "markorepairs"
end
test "updates the user's default post expiry", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "1"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["akkoma"]["status_ttl_days"] == 1
end
test "resets the user's default post expiry", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "-1"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert is_nil(user_data["akkoma"]["status_ttl_days"])
end
test "does not allow negative integers other than -1 for TTL", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "-2"})
assert json_response_and_validate_schema(conn, 403)
end
test "updates the user's AKAs", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{
"also_known_as" => ["https://mushroom.kingdom/users/mario"]
})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["also_known_as"] == ["https://mushroom.kingdom/users/mario"]
end
test "doesn't update non-url akas", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{
"also_known_as" => ["aReallyCoolGuy"]
})
assert json_response_and_validate_schema(conn, 403)
end
test "updates the user's avatar", %{user: user, conn: conn} do
new_avatar = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert user.avatar == %{}
res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["avatar"] != User.avatar_url(user)
user = User.get_by_id(user.id)
refute user.avatar == %{}
# Also resets it
_res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""})
user = User.get_by_id(user.id)
assert user.avatar == nil
end
test "updates the user's avatar, upload_limit, returns a HTTP 413", %{conn: conn, user: user} do
upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8
assert :ok ==
File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>)
new_avatar_oversized = %Plug.Upload{
content_type: nil,
path: Path.absname("test/tmp/large_binary.data"),
filename: "large_binary.data"
}
assert user.avatar == %{}
res =
patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar_oversized})
assert user_response = json_response_and_validate_schema(res, 413)
assert user_response["avatar"] != User.avatar_url(user)
user = User.get_by_id(user.id)
assert user.avatar == %{}
clear_config([:instance, :upload_limit], upload_limit)
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
test "updates the user's banner", %{user: user, conn: conn} do
new_header = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["header"] != User.banner_url(user)
# Also resets it
_res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""})
user = User.get_by_id(user.id)
assert user.banner == nil
end
test "updates the user's banner, upload_limit, returns a HTTP 413", %{conn: conn, user: user} do
upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8
assert :ok ==
File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>)
new_header_oversized = %Plug.Upload{
content_type: nil,
path: Path.absname("test/tmp/large_binary.data"),
filename: "large_binary.data"
}
res =
patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header_oversized})
assert user_response = json_response_and_validate_schema(res, 413)
assert user_response["header"] != User.banner_url(user)
user = User.get_by_id(user.id)
assert user.banner == %{}
clear_config([:instance, :upload_limit], upload_limit)
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
test "updates the user's background", %{conn: conn, user: user} do
new_header = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_header
})
assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["pleroma"]["background_image"]
#
# Also resets it
_res =
patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""})
user = User.get_by_id(user.id)
assert user.background == nil
end
test "updates the user's background, upload_limit, returns a HTTP 413", %{
conn: conn,
user: user
} do
upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8
assert :ok ==
File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>)
new_background_oversized = %Plug.Upload{
content_type: nil,
path: Path.absname("test/tmp/large_binary.data"),
filename: "large_binary.data"
}
res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_background_oversized
})
- assert user_response = json_response_and_validate_schema(res, 413)
+ assert json_response_and_validate_schema(res, 413)
assert user.background == %{}
clear_config([:instance, :upload_limit], upload_limit)
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
test "requires 'write:accounts' permission" do
token1 = insert(:oauth_token, scopes: ["read"])
token2 = insert(:oauth_token, scopes: ["write", "follow"])
for token <- [token1, token2] do
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer #{token.token}")
|> patch("/api/v1/accounts/update_credentials", %{})
if token == token1 do
assert %{"error" => "Insufficient permissions: write:accounts."} ==
json_response_and_validate_schema(conn, 403)
else
assert json_response_and_validate_schema(conn, 200)
end
end
end
test "updates profile emojos", %{user: user, conn: conn} do
note = "*sips :blank:*"
name = "I am :firefox:"
ret_conn =
patch(conn, "/api/v1/accounts/update_credentials", %{
"note" => note,
"display_name" => name
})
assert json_response_and_validate_schema(ret_conn, 200)
conn = get(conn, "/api/v1/accounts/#{user.id}")
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["note"] == note
assert user_data["display_name"] == name
assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"]
end
test "update fields", %{conn: conn} do
fields = [
- %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"},
- %{"name" => "link.io", "value" => "cofe.io"}
+ %{name: "<a href=\"http://google.com\">foo</a>", value: "<script>bar</script>"},
+ %{name: "link.io", value: "cofe.io"}
]
account_data =
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(200)
assert account_data["fields"] == [
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
%{
"name" => "link.io",
"value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)
}
]
assert account_data["source"]["fields"] == [
%{
"name" => "<a href=\"http://google.com\">foo</a>",
"value" => "<script>bar</script>"
},
%{"name" => "link.io", "value" => "cofe.io"}
]
end
test "emojis in fields labels", %{conn: conn} do
fields = [
- %{"name" => ":firefox:", "value" => "is best 2hu"},
- %{"name" => "they wins", "value" => ":blank:"}
+ %{name: ":firefox:", value: "is best 2hu"},
+ %{name: "they wins", value: ":blank:"}
]
account_data =
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(200)
assert account_data["fields"] == [
%{"name" => ":firefox:", "value" => "is best 2hu"},
%{"name" => "they wins", "value" => ":blank:"}
]
assert account_data["source"]["fields"] == [
%{"name" => ":firefox:", "value" => "is best 2hu"},
%{"name" => "they wins", "value" => ":blank:"}
]
assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"]
end
test "update fields via x-www-form-urlencoded", %{conn: conn} do
fields =
[
"fields_attributes[1][name]=link",
"fields_attributes[1][value]=http://cofe.io",
"fields_attributes[0][name]=foo",
"fields_attributes[0][value]=bar"
]
|> Enum.join("&")
account =
conn
|> put_req_header("content-type", "application/x-www-form-urlencoded")
|> patch("/api/v1/accounts/update_credentials", fields)
|> json_response_and_validate_schema(200)
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
%{
"name" => "link",
"value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>)
}
]
assert account["source"]["fields"] == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "link", "value" => "http://cofe.io"}
]
end
test "update fields with empty name", %{conn: conn} do
fields = [
- %{"name" => "foo", "value" => ""},
- %{"name" => "", "value" => "bar"}
+ %{name: "foo", value: ""},
+ %{name: "", value: "bar"}
]
account =
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(200)
assert account["fields"] == [
%{"name" => "foo", "value" => ""}
]
end
test "update fields when invalid request", %{conn: conn} do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join()
- fields = [%{"name" => "foo", "value" => long_value}]
+ fields = [%{name: "foo", value: long_value}]
assert %{"error" => "Invalid request"} ==
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(403)
- fields = [%{"name" => long_name, "value" => "bar"}]
+ fields = [%{name: long_name, value: "bar"}]
assert %{"error" => "Invalid request"} ==
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(403)
clear_config([:instance, :max_account_fields], 1)
fields = [
- %{"name" => "foo", "value" => "bar"},
+ %{name: "foo", value: "bar"},
%{"name" => "link", "value" => "cofe.io"}
]
assert %{"error" => "Invalid request"} ==
conn
- |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+ |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: fields})
|> json_response_and_validate_schema(403)
end
end
describe "Mark account as bot" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "changing actor_type to Service makes account a bot", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"})
|> json_response_and_validate_schema(200)
assert account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Service"
end
test "changing actor_type to Person makes account a human", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"})
|> json_response_and_validate_schema(200)
refute account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Person"
end
test "changing actor_type to Application causes error", %{conn: conn} do
response =
conn
|> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"})
|> json_response_and_validate_schema(403)
assert %{"error" => "Invalid request"} == response
end
test "changing bot field to true changes actor_type to Service", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{bot: "true"})
|> json_response_and_validate_schema(200)
assert account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Service"
end
test "changing bot field to false changes actor_type to Person", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{bot: "false"})
|> json_response_and_validate_schema(200)
refute account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Person"
end
test "actor_type field has a higher priority than bot", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{
actor_type: "Person",
bot: "true"
})
|> json_response_and_validate_schema(200)
refute account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Person"
end
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
index ad271c31b..89a2cf634 100644
--- a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
@@ -1,282 +1,282 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Swoosh.TestAssertions
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do
{:ok, user} =
insert(:user)
|> User.confirmation_changeset(set_confirmation: false)
|> User.update_and_set_cache()
refute user.is_confirmed
[user: user]
end
setup do: clear_config([:instance, :account_activation_required], true)
test "resend account confirmation email", %{conn: conn, user: user} do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}")
|> json_response_and_validate_schema(:no_content)
ObanHelpers.perform_all()
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
test "resend account confirmation email (with nickname)", %{conn: conn, user: user} do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/accounts/confirmation_resend?nickname=#{user.nickname}")
|> json_response_and_validate_schema(:no_content)
ObanHelpers.perform_all()
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
end
describe "getting favorites timeline of specified user" do
setup do
[current_user, user] = insert_pair(:user, hide_favorites: false)
%{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user)
[current_user: current_user, user: user, conn: conn]
end
test "returns list of statuses favorited by specified user", %{
conn: conn,
user: user
} do
[activity | _] = insert_pair(:note_activity)
CommonAPI.favorite(user, activity.id)
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(:ok)
[like] = response
assert length(response) == 1
assert like["id"] == activity.id
end
test "returns favorites for specified user_id when requester is not logged in", %{
user: user
} do
activity = insert(:note_activity)
CommonAPI.favorite(user, activity.id)
response =
build_conn()
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(200)
assert length(response) == 1
end
test "returns favorited DM only when user is logged in and he is one of recipients", %{
current_user: current_user,
user: user
} do
{:ok, direct} =
CommonAPI.post(current_user, %{
status: "Hi @#{user.nickname}!",
visibility: "direct"
})
CommonAPI.favorite(user, direct.id)
for u <- [user, current_user] do
response =
build_conn()
|> assign(:user, u)
|> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"]))
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(:ok)
assert length(response) == 1
end
response =
build_conn()
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(200)
- assert length(response) == 0
+ assert response == []
end
test "does not return others' favorited DM when user is not one of recipients", %{
conn: conn,
user: user
} do
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_two, %{
status: "Hi @#{user.nickname}!",
visibility: "direct"
})
CommonAPI.favorite(user, direct.id)
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "paginates favorites using since_id and max_id", %{
conn: conn,
user: user
} do
activities = insert_list(10, :note_activity)
Enum.each(activities, fn activity ->
CommonAPI.favorite(user, activity.id)
end)
third_activity = Enum.at(activities, 2)
seventh_activity = Enum.at(activities, 6)
response =
conn
|> get(
"/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{seventh_activity.id}"
)
|> json_response_and_validate_schema(:ok)
assert length(response) == 3
refute third_activity in response
refute seventh_activity in response
end
test "limits favorites using limit parameter", %{
conn: conn,
user: user
} do
7
|> insert_list(:note_activity)
|> Enum.each(fn activity ->
CommonAPI.favorite(user, activity.id)
end)
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites?limit=3")
|> json_response_and_validate_schema(:ok)
assert length(response) == 3
end
test "returns empty response when user does not have any favorited statuses", %{
conn: conn,
user: user
} do
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "returns 404 error when specified user is not exist", %{conn: conn} do
conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "returns 403 error when user has hidden own favorites", %{conn: conn} do
user = insert(:user, hide_favorites: true)
activity = insert(:note_activity)
CommonAPI.favorite(user, activity.id)
conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"}
end
test "hides favorites for new users by default", %{conn: conn} do
user = insert(:user)
activity = insert(:note_activity)
CommonAPI.favorite(user, activity.id)
assert user.hide_favorites
conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"}
end
end
describe "subscribing / unsubscribing" do
test "subscribing / unsubscribing to a user" do
%{user: user, conn: conn} = oauth_access(["follow"])
subscription_target = insert(:user)
ret_conn =
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
assert %{"id" => _id, "subscribing" => true} =
json_response_and_validate_schema(ret_conn, 200)
conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
assert %{"id" => _id, "subscribing" => false} = json_response_and_validate_schema(conn, 200)
end
end
describe "subscribing" do
test "returns 404 when subscription_target not found" do
%{conn: conn} = oauth_access(["write:follows"])
conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
end
describe "unsubscribing" do
test "returns 404 when subscription_target not found" do
%{conn: conn} = oauth_access(["follow"])
conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
index 61a44b8ba..0d3d38b33 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs
@@ -1,381 +1,381 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do
use Pleroma.Web.ConnCase, async: false
import Mock
import Tesla.Mock
import Pleroma.Factory
@emoji_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
setup do: clear_config([:instance, :public], true)
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
admin_conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
Pleroma.Emoji.reload()
{:ok, %{admin_conn: admin_conn}}
end
- describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do
+ describe "POST/PATCH/DELETE /api/v1/pleroma/emoji/packs/files?name=:name" do
setup do
pack_file = "#{@emoji_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
end)
:ok
end
test "upload zip file with emojies", %{admin_conn: admin_conn} do
on_exit(fn ->
[
"128px/a_trusted_friend-128.png",
"auroraborealis.png",
"1000px/baby_in_a_box.png",
"1000px/bear.png",
"128px/bear-128.png"
]
|> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end)
end)
resp =
admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
file: %Plug.Upload{
content_type: "application/zip",
filename: "emojis.zip",
path: Path.absname("test/fixtures/emojis.zip")
}
})
|> json_response_and_validate_schema(200)
assert resp == %{
"a_trusted_friend-128" => "128px/a_trusted_friend-128.png",
"auroraborealis" => "auroraborealis.png",
"baby_in_a_box" => "1000px/baby_in_a_box.png",
"bear" => "1000px/bear.png",
"bear-128" => "128px/bear-128.png",
"blank" => "blank.png",
"blank2" => "blank2.png"
}
Enum.each(Map.values(resp), fn path ->
assert File.exists?("#{@emoji_path}/test_pack/#{path}")
end)
end
test "create shortcode exists", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(:conflict) == %{
"error" => "An emoji with the \"blank\" shortcode already exists"
}
end
test "don't rewrite old emoji", %{admin_conn: admin_conn} do
on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank3",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(200) == %{
"blank" => "blank.png",
"blank2" => "blank2.png",
"blank3" => "dir/blank.png"
}
assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank",
new_shortcode: "blank2",
new_filename: "dir_2/blank_3.png"
})
|> json_response_and_validate_schema(:conflict) == %{
"error" =>
"New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option"
}
end
test "rewrite old emoji with force option", %{admin_conn: admin_conn} do
on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank3",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(200) == %{
"blank" => "blank.png",
"blank2" => "blank2.png",
"blank3" => "dir/blank.png"
}
assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank3",
new_shortcode: "blank4",
new_filename: "dir_2/blank_3.png",
force: true
})
|> json_response_and_validate_schema(200) == %{
"blank" => "blank.png",
"blank2" => "blank2.png",
"blank4" => "dir_2/blank_3.png"
}
assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png")
end
test "with empty filename", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank2",
filename: "",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(422) == %{
"error" => "pack name, shortcode or filename cannot be empty"
}
end
test "add file with not loaded pack", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=not_loaded", %{
shortcode: "blank3",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(:not_found) == %{
"error" => "pack \"not_loaded\" is not found"
}
end
test "returns an error on add file when file system is not writable", %{
admin_conn: admin_conn
} do
pack_file = Path.join([@emoji_path, "not_loaded", "pack.json"])
with_mocks([
{File, [:passthrough], [stat: fn ^pack_file -> {:error, :eacces} end]}
]) do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=not_loaded", %{
shortcode: "blank3",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(500) == %{
"error" =>
"Unexpected error occurred while adding file to pack. (POSIX error: Permission denied)"
}
end
end
test "remove file with not loaded pack", %{admin_conn: admin_conn} do
assert admin_conn
- |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3")
+ |> delete("/api/v1/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3")
|> json_response_and_validate_schema(:not_found) == %{
"error" => "pack \"not_loaded\" is not found"
}
end
test "remove file with empty shortcode", %{admin_conn: admin_conn} do
assert admin_conn
- |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=")
+ |> delete("/api/v1/pleroma/emoji/packs/files?name=not_loaded&shortcode=")
|> json_response_and_validate_schema(:not_found) == %{
"error" => "pack \"not_loaded\" is not found"
}
end
test "update file with not loaded pack", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=not_loaded", %{
shortcode: "blank4",
new_shortcode: "blank3",
new_filename: "dir_2/blank_3.png"
})
|> json_response_and_validate_schema(:not_found) == %{
"error" => "pack \"not_loaded\" is not found"
}
end
test "new with shortcode as file with update", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank4",
filename: "dir/blank.png",
file: %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_path}/test_pack/blank.png"
}
})
|> json_response_and_validate_schema(200) == %{
"blank" => "blank.png",
"blank4" => "dir/blank.png",
"blank2" => "blank2.png"
}
assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank4",
new_shortcode: "blank3",
new_filename: "dir_2/blank_3.png"
})
|> json_response_and_validate_schema(200) == %{
"blank3" => "dir_2/blank_3.png",
"blank" => "blank.png",
"blank2" => "blank2.png"
}
refute File.exists?("#{@emoji_path}/test_pack/dir/")
assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png")
assert admin_conn
- |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3")
+ |> delete("/api/v1/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3")
|> json_response_and_validate_schema(200) == %{
"blank" => "blank.png",
"blank2" => "blank2.png"
}
refute File.exists?("#{@emoji_path}/test_pack/dir_2/")
on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end)
end
test "new with shortcode from url", %{admin_conn: admin_conn} do
mock(fn
%{
method: :get,
url: "https://test-blank/blank_url.png"
} ->
text(File.read!("#{@emoji_path}/test_pack/blank.png"))
end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank_url",
file: "https://test-blank/blank_url.png"
})
|> json_response_and_validate_schema(200) == %{
"blank_url" => "blank_url.png",
"blank" => "blank.png",
"blank2" => "blank2.png"
}
assert File.exists?("#{@emoji_path}/test_pack/blank_url.png")
on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end)
end
test "new without shortcode", %{admin_conn: admin_conn} do
on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> post("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
file: %Plug.Upload{
filename: "shortcode.png",
path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png"
}
})
|> json_response_and_validate_schema(200) == %{
"shortcode" => "shortcode.png",
"blank" => "blank.png",
"blank2" => "blank2.png"
}
end
test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do
assert admin_conn
- |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3")
+ |> delete("/api/v1/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3")
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "Emoji \"blank3\" does not exist"
}
end
test "update non existing emoji", %{admin_conn: admin_conn} do
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank3",
new_shortcode: "blank4",
new_filename: "dir_2/blank_3.png"
})
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "Emoji \"blank3\" does not exist"
}
end
test "update with empty shortcode", %{admin_conn: admin_conn} do
assert %{
"error" => "Missing field: new_shortcode."
} =
admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/packs/files?name=test_pack", %{
shortcode: "blank",
new_filename: "dir_2/blank_3.png"
})
|> json_response_and_validate_schema(:bad_request)
end
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
index d1ba067b8..b86418196 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
@@ -1,658 +1,676 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
use Pleroma.Web.ConnCase, async: false
import Mock
import Tesla.Mock
import Pleroma.Factory
@emoji_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
setup do: clear_config([:instance, :public], true)
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
admin_conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
Pleroma.Emoji.reload()
{:ok, %{admin_conn: admin_conn}}
end
- test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do
+ test "GET /api/v1/pleroma/emoji/packs when :public: false", %{conn: conn} do
clear_config([:instance, :public], false)
- conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
+ conn |> get("/api/v1/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
end
- test "GET /api/pleroma/emoji/packs", %{conn: conn} do
- resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
+ test "GET /api/v1/pleroma/emoji/packs", %{conn: conn} do
+ resp =
+ conn
+ |> get("/api/v1/pleroma/emoji/packs")
+ |> json_response_and_validate_schema(200)
assert resp["count"] == 4
assert resp["packs"]
|> Map.keys()
|> length() == 4
shared = resp["packs"]["test_pack"]
assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"}
assert Map.has_key?(shared["pack"], "download-sha256")
assert shared["pack"]["can-download"]
assert shared["pack"]["share-files"]
non_shared = resp["packs"]["test_pack_nonshared"]
assert non_shared["pack"]["share-files"] == false
assert non_shared["pack"]["can-download"] == false
resp =
conn
- |> get("/api/pleroma/emoji/packs?page_size=1")
+ |> get("/api/v1/pleroma/emoji/packs?page_size=1")
|> json_response_and_validate_schema(200)
assert resp["count"] == 4
packs = Map.keys(resp["packs"])
assert length(packs) == 1
[pack1] = packs
resp =
conn
- |> get("/api/pleroma/emoji/packs?page_size=1&page=2")
+ |> get("/api/v1/pleroma/emoji/packs?page_size=1&page=2")
|> json_response_and_validate_schema(200)
assert resp["count"] == 4
packs = Map.keys(resp["packs"])
assert length(packs) == 1
[pack2] = packs
resp =
conn
- |> get("/api/pleroma/emoji/packs?page_size=1&page=3")
+ |> get("/api/v1/pleroma/emoji/packs?page_size=1&page=3")
|> json_response_and_validate_schema(200)
assert resp["count"] == 4
packs = Map.keys(resp["packs"])
assert length(packs) == 1
[pack3] = packs
resp =
conn
- |> get("/api/pleroma/emoji/packs?page_size=1&page=4")
+ |> get("/api/v1/pleroma/emoji/packs?page_size=1&page=4")
|> json_response_and_validate_schema(200)
assert resp["count"] == 4
packs = Map.keys(resp["packs"])
assert length(packs) == 1
[pack4] = packs
assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4
end
- describe "GET /api/pleroma/emoji/packs/remote" do
+ describe "GET /api/v1/pleroma/emoji/packs/remote" do
test "shareable instance", %{admin_conn: admin_conn, conn: conn} do
resp =
conn
- |> get("/api/pleroma/emoji/packs?page=2&page_size=1")
+ |> get("/api/v1/pleroma/emoji/packs?page=2&page_size=1")
|> json_response_and_validate_schema(200)
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
- %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} ->
+ %{
+ method: :get,
+ url: "https://example.com/api/v1/pleroma/emoji/packs?page=2&page_size=1"
+ } ->
json(resp)
end)
assert admin_conn
- |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1")
+ |> get(
+ "/api/v1/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1"
+ )
|> json_response_and_validate_schema(200) == resp
end
test "non shareable instance", %{admin_conn: admin_conn} do
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: []}})
end)
assert admin_conn
- |> get("/api/pleroma/emoji/packs/remote?url=https://example.com")
+ |> get("/api/v1/pleroma/emoji/packs/remote?url=https://example.com")
|> json_response_and_validate_schema(500) == %{
"error" => "The requested instance does not support sharing emoji packs"
}
end
end
- describe "GET /api/pleroma/emoji/packs/archive?name=:name" do
+ describe "GET /api/v1/pleroma/emoji/packs/archive?name=:name" do
test "download shared pack", %{conn: conn} do
resp =
conn
- |> get("/api/pleroma/emoji/packs/archive?name=test_pack")
+ |> get("/api/v1/pleroma/emoji/packs/archive?name=test_pack")
|> response(200)
{:ok, arch} = :zip.unzip(resp, [:memory])
assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
test "non existing pack", %{conn: conn} do
assert conn
- |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import")
+ |> get("/api/v1/pleroma/emoji/packs/archive?name=test_pack_for_import")
|> json_response_and_validate_schema(:not_found) == %{
"error" => "Pack test_pack_for_import does not exist"
}
end
test "non downloadable pack", %{conn: conn} do
assert conn
- |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared")
+ |> get("/api/v1/pleroma/emoji/packs/archive?name=test_pack_nonshared")
|> json_response_and_validate_schema(:forbidden) == %{
"error" =>
"Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
}
end
end
- describe "POST /api/pleroma/emoji/packs/download" do
+ describe "POST /api/v1/pleroma/emoji/packs/download" do
test "shared pack from remote and non shared from fallback-src", %{
admin_conn: admin_conn,
conn: conn
} do
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack"
} ->
conn
- |> get("/api/pleroma/emoji/pack?name=test_pack")
+ |> get("/api/v1/pleroma/emoji/pack?name=test_pack")
|> json_response_and_validate_schema(200)
|> json()
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=test_pack"
} ->
conn
- |> get("/api/pleroma/emoji/packs/archive?name=test_pack")
+ |> get("/api/v1/pleroma/emoji/packs/archive?name=test_pack")
|> response(200)
|> text()
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack_nonshared"
} ->
conn
- |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared")
+ |> get("/api/v1/pleroma/emoji/pack?name=test_pack_nonshared")
|> json_response_and_validate_schema(200)
|> json()
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip"))
end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/download", %{
+ |> post("/api/v1/pleroma/emoji/packs/download", %{
url: "https://example.com",
name: "test_pack",
as: "test_pack2"
})
|> json_response_and_validate_schema(200) == "ok"
assert File.exists?("#{@emoji_path}/test_pack2/pack.json")
assert File.exists?("#{@emoji_path}/test_pack2/blank.png")
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name=test_pack2")
+ |> delete("/api/v1/pleroma/emoji/pack?name=test_pack2")
|> json_response_and_validate_schema(200) == "ok"
refute File.exists?("#{@emoji_path}/test_pack2")
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
- "/api/pleroma/emoji/packs/download",
+ "/api/v1/pleroma/emoji/packs/download",
%{
url: "https://example.com",
name: "test_pack_nonshared",
as: "test_pack_nonshared2"
}
)
|> json_response_and_validate_schema(200) == "ok"
assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json")
assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png")
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2")
+ |> delete("/api/v1/pleroma/emoji/pack?name=test_pack_nonshared2")
|> json_response_and_validate_schema(200) == "ok"
refute File.exists?("#{@emoji_path}/test_pack_nonshared2")
end
test "nonshareable instance", %{admin_conn: admin_conn} do
mock(fn
%{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: []}})
end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
- "/api/pleroma/emoji/packs/download",
+ "/api/v1/pleroma/emoji/packs/download",
%{
url: "https://old-instance",
name: "test_pack",
as: "test_pack2"
}
)
|> json_response_and_validate_schema(500) == %{
"error" => "The requested instance does not support sharing emoji packs"
}
end
test "checksum fail", %{admin_conn: admin_conn} do
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=pack_bad_sha"
} ->
{:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha")
%Tesla.Env{status: 200, body: Jason.encode!(pack)}
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha"
+ url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=pack_bad_sha"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip")
}
end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/download", %{
+ |> post("/api/v1/pleroma/emoji/packs/download", %{
url: "https://example.com",
name: "pack_bad_sha",
as: "pack_bad_sha2"
})
|> json_response_and_validate_schema(:internal_server_error) == %{
"error" => "SHA256 for the pack doesn't match the one sent by the server"
}
end
test "other error", %{admin_conn: admin_conn} do
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack"
} ->
{:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack")
%Tesla.Env{status: 200, body: Jason.encode!(pack)}
end)
assert admin_conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/emoji/packs/download", %{
+ |> post("/api/v1/pleroma/emoji/packs/download", %{
url: "https://example.com",
name: "test_pack",
as: "test_pack2"
})
|> json_response_and_validate_schema(:internal_server_error) == %{
"error" =>
"The pack was not set as shared and there is no fallback src to download from"
}
end
end
- describe "PATCH/update /api/pleroma/emoji/pack?name=:name" do
+ describe "PATCH/update /api/v1/pleroma/emoji/pack?name=:name" do
setup do
pack_file = "#{@emoji_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
end)
{:ok,
pack_file: pack_file,
new_data: %{
"license" => "Test license changed",
"homepage" => "https://pleroma.social",
"description" => "Test description",
"share-files" => false
}}
end
test "returns error when file system not writable", %{admin_conn: conn} = ctx do
with_mocks([
{File, [:passthrough], [stat: fn _ -> {:error, :eacces} end]}
]) do
assert conn
|> put_req_header("content-type", "multipart/form-data")
|> patch(
- "/api/pleroma/emoji/pack?name=test_pack",
+ "/api/v1/pleroma/emoji/pack?name=test_pack",
%{"metadata" => ctx[:new_data]}
)
|> json_response_and_validate_schema(500)
end
end
test "for a pack without a fallback source", ctx do
assert ctx[:admin_conn]
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/pack?name=test_pack", %{
+ |> patch("/api/v1/pleroma/emoji/pack?name=test_pack", %{
"metadata" => ctx[:new_data]
})
|> json_response_and_validate_schema(200) == ctx[:new_data]
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
end
test "for a pack with a fallback source", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip"))
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
new_data_with_sha =
Map.put(
new_data,
"fallback-src-sha256",
"1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D"
)
assert ctx[:admin_conn]
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data})
+ |> patch("/api/v1/pleroma/emoji/pack?name=test_pack", %{metadata: new_data})
|> json_response_and_validate_schema(200) == new_data_with_sha
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
end
test "when the fallback source doesn't have all the files", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
{:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory])
text(empty_arch)
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
assert ctx[:admin_conn]
|> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data})
+ |> patch("/api/v1/pleroma/emoji/pack?name=test_pack", %{metadata: new_data})
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "The fallback archive does not have all files specified in pack.json"
}
end
end
- describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do
+ describe "POST/DELETE /api/v1/pleroma/emoji/pack?name=:name" do
test "returns an error on creates pack when file system not writable", %{
admin_conn: admin_conn
} do
path_pack = Path.join(@emoji_path, "test_pack")
with_mocks([
{File, [:passthrough], [mkdir: fn ^path_pack -> {:error, :eacces} end]}
]) do
assert admin_conn
- |> post("/api/pleroma/emoji/pack?name=test_pack")
+ |> post("/api/v1/pleroma/emoji/pack?name=test_pack")
|> json_response_and_validate_schema(500) == %{
"error" =>
"Unexpected error occurred while creating pack. (POSIX error: Permission denied)"
}
end
end
test "returns an error on deletes pack when the file system is not writable", %{
admin_conn: admin_conn
} do
path_pack = Path.join(@emoji_path, "test_emoji_pack")
try do
{:ok, _pack} = Pleroma.Emoji.Pack.create("test_emoji_pack")
with_mocks([
{File, [:passthrough], [rm_rf: fn ^path_pack -> {:error, :eacces, path_pack} end]}
]) do
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name=test_emoji_pack")
+ |> delete("/api/v1/pleroma/emoji/pack?name=test_emoji_pack")
|> json_response_and_validate_schema(500) == %{
"error" =>
"Couldn't delete the `test_emoji_pack` pack (POSIX error: Permission denied)"
}
end
after
File.rm_rf(path_pack)
end
end
test "creating and deleting a pack", %{admin_conn: admin_conn} do
assert admin_conn
- |> post("/api/pleroma/emoji/pack?name=test_created")
+ |> post("/api/v1/pleroma/emoji/pack?name=test_created")
|> json_response_and_validate_schema(200) == "ok"
assert File.exists?("#{@emoji_path}/test_created/pack.json")
assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{
"pack" => %{},
"files" => %{},
"files_count" => 0
}
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name=test_created")
+ |> delete("/api/v1/pleroma/emoji/pack?name=test_created")
|> json_response_and_validate_schema(200) == "ok"
refute File.exists?("#{@emoji_path}/test_created/pack.json")
end
test "if pack exists", %{admin_conn: admin_conn} do
path = Path.join(@emoji_path, "test_created")
File.mkdir(path)
pack_file = Jason.encode!(%{files: %{}, pack: %{}})
File.write!(Path.join(path, "pack.json"), pack_file)
assert admin_conn
- |> post("/api/pleroma/emoji/pack?name=test_created")
+ |> post("/api/v1/pleroma/emoji/pack?name=test_created")
|> json_response_and_validate_schema(:conflict) == %{
"error" => "A pack named \"test_created\" already exists"
}
on_exit(fn -> File.rm_rf(path) end)
end
test "with empty name", %{admin_conn: admin_conn} do
assert admin_conn
- |> post("/api/pleroma/emoji/pack?name= ")
+ |> post("/api/v1/pleroma/emoji/pack?name= ")
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "pack name cannot be empty"
}
end
end
test "deleting nonexisting pack", %{admin_conn: admin_conn} do
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name=non_existing")
+ |> delete("/api/v1/pleroma/emoji/pack?name=non_existing")
|> json_response_and_validate_schema(:not_found) == %{
"error" => "Pack non_existing does not exist"
}
end
test "deleting with empty name", %{admin_conn: admin_conn} do
assert admin_conn
- |> delete("/api/pleroma/emoji/pack?name= ")
+ |> delete("/api/v1/pleroma/emoji/pack?name= ")
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "pack name cannot be empty"
}
end
test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
on_exit(fn ->
File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt")
File.rm!("#{@emoji_path}/test_pack_for_import/pack.json")
end)
- resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
+ resp =
+ conn
+ |> get("/api/v1/pleroma/emoji/packs")
+ |> json_response_and_validate_schema(200)
refute Map.has_key?(resp["packs"], "test_pack_for_import")
assert admin_conn
- |> get("/api/pleroma/emoji/packs/import")
+ |> get("/api/v1/pleroma/emoji/packs/import")
|> json_response_and_validate_schema(200) == ["test_pack_for_import"]
- resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
+ resp =
+ conn
+ |> get("/api/v1/pleroma/emoji/packs")
+ |> json_response_and_validate_schema(200)
+
assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"}
File.rm!("#{@emoji_path}/test_pack_for_import/pack.json")
refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json")
emoji_txt_content = """
blank, blank.png, Fun
blank2, blank.png
foo, /emoji/test_pack_for_import/blank.png
bar
"""
File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
assert admin_conn
- |> get("/api/pleroma/emoji/packs/import")
+ |> get("/api/v1/pleroma/emoji/packs/import")
|> json_response_and_validate_schema(200) == ["test_pack_for_import"]
- resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
+ resp =
+ conn
+ |> get("/api/v1/pleroma/emoji/packs")
+ |> json_response_and_validate_schema(200)
assert resp["packs"]["test_pack_for_import"]["files"] == %{
"blank" => "blank.png",
"blank2" => "blank.png",
"foo" => "blank.png"
}
end
- describe "GET /api/pleroma/emoji/pack?name=:name" do
+ describe "GET /api/v1/pleroma/emoji/pack?name=:name" do
test "shows pack.json", %{conn: conn} do
assert %{
"files" => files,
"files_count" => 2,
"pack" => %{
"can-download" => true,
"description" => "Test description",
"download-sha256" => _,
"homepage" => "https://pleroma.social",
"license" => "Test license",
"share-files" => true
}
} =
conn
- |> get("/api/pleroma/emoji/pack?name=test_pack")
+ |> get("/api/v1/pleroma/emoji/pack?name=test_pack")
|> json_response_and_validate_schema(200)
assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"}
assert %{
"files" => files,
"files_count" => 2
} =
conn
- |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1")
+ |> get("/api/v1/pleroma/emoji/pack?name=test_pack&page_size=1")
|> json_response_and_validate_schema(200)
assert files |> Map.keys() |> length() == 1
assert %{
"files" => files,
"files_count" => 2
} =
conn
- |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2")
+ |> get("/api/v1/pleroma/emoji/pack?name=test_pack&page_size=1&page=2")
|> json_response_and_validate_schema(200)
assert files |> Map.keys() |> length() == 1
end
test "for pack name with special chars", %{conn: conn} do
assert %{
"files" => _files,
"files_count" => 1,
"pack" => %{
"can-download" => true,
"description" => "Test description",
"download-sha256" => _,
"homepage" => "https://pleroma.social",
"license" => "Test license",
"share-files" => true
}
} =
conn
- |> get("/api/pleroma/emoji/pack?name=blobs.gg")
+ |> get("/api/v1/pleroma/emoji/pack?name=blobs.gg")
|> json_response_and_validate_schema(200)
end
test "non existing pack", %{conn: conn} do
assert conn
- |> get("/api/pleroma/emoji/pack?name=non_existing")
+ |> get("/api/v1/pleroma/emoji/pack?name=non_existing")
|> json_response_and_validate_schema(:not_found) == %{
"error" => "Pack non_existing does not exist"
}
end
test "error name", %{conn: conn} do
assert conn
- |> get("/api/pleroma/emoji/pack?name= ")
+ |> get("/api/v1/pleroma/emoji/pack?name= ")
|> json_response_and_validate_schema(:bad_request) == %{
"error" => "pack name cannot be empty"
}
end
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs
index 24074f4e5..3716fcdec 100644
--- a/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs
@@ -1,264 +1,279 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.MFA.Settings
alias Pleroma.MFA.TOTP
describe "GET /api/pleroma/accounts/mfa/settings" do
test "returns user mfa settings for new user", %{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "follow"])
token2 = insert(:oauth_token, scopes: ["write"])
assert conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> get("/api/pleroma/accounts/mfa")
|> json_response(:ok) == %{
"settings" => %{"enabled" => false, "totp" => false}
}
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
|> get("/api/pleroma/accounts/mfa")
|> json_response(403) == %{
"error" => "Insufficient permissions: read:security."
}
end
test "returns user mfa settings with enabled totp", %{conn: conn} do
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
enabled: true,
totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true}
}
)
token = insert(:oauth_token, scopes: ["read", "follow"], user: user)
assert conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> get("/api/pleroma/accounts/mfa")
|> json_response(:ok) == %{
"settings" => %{"enabled" => true, "totp" => true}
}
end
end
describe "GET /api/pleroma/accounts/mfa/backup_codes" do
test "returns backup codes", %{conn: conn} do
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
backup_codes: ["1", "2", "3"],
totp: %Settings.TOTP{secret: "secret"}
}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
token2 = insert(:oauth_token, scopes: ["read"])
response =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> get("/api/pleroma/accounts/mfa/backup_codes")
|> json_response(:ok)
assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"]
user = refresh_record(user)
mfa_settings = user.multi_factor_authentication_settings
assert mfa_settings.totp.secret == "secret"
refute mfa_settings.backup_codes == ["1", "2", "3"]
refute mfa_settings.backup_codes == []
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
|> get("/api/pleroma/accounts/mfa/backup_codes")
|> json_response(403) == %{
"error" => "Insufficient permissions: write:security."
}
end
end
describe "GET /api/pleroma/accounts/mfa/setup/totp" do
test "return errors when method is invalid", %{conn: conn} do
user = insert(:user)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
response =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> get("/api/pleroma/accounts/mfa/setup/torf")
|> json_response(400)
assert response == %{"error" => "undefined method"}
end
test "returns key and provisioning_uri", %{conn: conn} do
user =
insert(:user,
multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
token2 = insert(:oauth_token, scopes: ["read"])
response =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> get("/api/pleroma/accounts/mfa/setup/totp")
|> json_response(:ok)
user = refresh_record(user)
mfa_settings = user.multi_factor_authentication_settings
secret = mfa_settings.totp.secret
refute mfa_settings.enabled
assert mfa_settings.backup_codes == ["1", "2", "3"]
assert response == %{
"key" => secret,
"provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}")
}
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
|> get("/api/pleroma/accounts/mfa/setup/totp")
|> json_response(403) == %{
"error" => "Insufficient permissions: write:security."
}
end
end
describe "GET /api/pleroma/accounts/mfa/confirm/totp" do
test "returns success result", %{conn: conn} do
secret = TOTP.generate_secret()
code = TOTP.generate_token(secret)
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
backup_codes: ["1", "2", "3"],
totp: %Settings.TOTP{secret: secret}
}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
token2 = insert(:oauth_token, scopes: ["read"])
assert conn
|> put_req_header("authorization", "Bearer #{token.token}")
- |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{
+ password: "test",
+ code: code
+ })
|> json_response(:ok)
settings = refresh_record(user).multi_factor_authentication_settings
assert settings.enabled
assert settings.totp.secret == secret
assert settings.totp.confirmed
assert settings.backup_codes == ["1", "2", "3"]
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
- |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{
+ password: "test",
+ code: code
+ })
|> json_response(403) == %{
"error" => "Insufficient permissions: write:security."
}
end
test "returns error if password incorrect", %{conn: conn} do
secret = TOTP.generate_secret()
code = TOTP.generate_token(secret)
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
backup_codes: ["1", "2", "3"],
totp: %Settings.TOTP{secret: secret}
}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
response =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
- |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code})
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{
+ password: "xxx",
+ code: code
+ })
|> json_response(422)
settings = refresh_record(user).multi_factor_authentication_settings
refute settings.enabled
refute settings.totp.confirmed
assert settings.backup_codes == ["1", "2", "3"]
assert response == %{"error" => "Invalid password."}
end
test "returns error if code incorrect", %{conn: conn} do
secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
backup_codes: ["1", "2", "3"],
totp: %Settings.TOTP{secret: secret}
}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
token2 = insert(:oauth_token, scopes: ["read"])
response =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
- |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{
+ password: "test",
+ code: "code"
+ })
|> json_response(422)
settings = refresh_record(user).multi_factor_authentication_settings
refute settings.enabled
refute settings.totp.confirmed
assert settings.backup_codes == ["1", "2", "3"]
assert response == %{"error" => "invalid_token"}
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
- |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{
+ password: "test",
+ code: "code"
+ })
|> json_response(403) == %{
"error" => "Insufficient permissions: write:security."
}
end
end
describe "DELETE /api/pleroma/accounts/mfa/totp" do
test "returns success result", %{conn: conn} do
user =
insert(:user,
multi_factor_authentication_settings: %Settings{
backup_codes: ["1", "2", "3"],
totp: %Settings.TOTP{secret: "secret"}
}
)
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
token2 = insert(:oauth_token, scopes: ["read"])
assert conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
|> json_response(:ok)
settings = refresh_record(user).multi_factor_authentication_settings
refute settings.enabled
assert settings.totp.secret == nil
refute settings.totp.confirmed
assert conn
|> put_req_header("authorization", "Bearer #{token2.token}")
|> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
|> json_response(403) == %{
"error" => "Insufficient permissions: write:security."
}
end
end
end
diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs
index 4d98de2bd..b77fa25f2 100644
--- a/test/pleroma/web/plugs/remote_ip_test.exs
+++ b/test/pleroma/web/plugs/remote_ip_test.exs
@@ -1,108 +1,120 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RemoteIpTest do
use ExUnit.Case
use Plug.Test
alias Pleroma.Web.Plugs.RemoteIp
import Pleroma.Tests.Helpers, only: [clear_config: 2]
- setup do:
- clear_config(RemoteIp,
- enabled: true,
- headers: ["x-forwarded-for"],
- proxies: [],
- reserved: [
- "127.0.0.0/8",
- "::1/128",
- "fc00::/7",
- "10.0.0.0/8",
- "172.16.0.0/12",
- "192.168.0.0/16"
- ]
- )
+ setup do
+ clear_config([RemoteIp, :enabled], true)
+ clear_config([RemoteIp, :headers], ["x-forwarded-for"])
+ clear_config([RemoteIp, :proxies], [])
+
+ clear_config(
+ [RemoteIp, :reserved],
+ [
+ "127.0.0.0/8",
+ "::1/128",
+ "fc00::/7",
+ "10.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16"
+ ]
+ )
+ end
test "disabled" do
- clear_config(RemoteIp, enabled: false)
+ clear_config([RemoteIp, :enabled], false)
%{remote_ip: remote_ip} = conn(:get, "/")
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == remote_ip
end
test "enabled" do
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom headers" do
- clear_config(RemoteIp, enabled: true, headers: ["cf-connecting-ip"])
+ clear_config([RemoteIp, :enabled], true)
+ clear_config([RemoteIp, :headers], ["cf-connecting-ip"])
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("cf-connecting-ip", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom proxies" do
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
clear_config([RemoteIp, :proxies], ["173.245.48.0/20"])
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "proxies set without CIDR format" do
clear_config([RemoteIp, :proxies], ["173.245.48.1"])
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "proxies set `nonsensical` CIDR" do
clear_config([RemoteIp, :reserved], ["127.0.0.0/8"])
clear_config([RemoteIp, :proxies], ["10.0.0.3/24"])
conn =
- conn(:get, "/")
+ :get
+ |> conn("/")
|> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
end
diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/twitter_api/password_controller_test.exs
index 45ab10a8a..05c3561bf 100644
--- a/test/pleroma/web/twitter_api/password_controller_test.exs
+++ b/test/pleroma/web/twitter_api/password_controller_test.exs
@@ -1,213 +1,213 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
import Swoosh.TestAssertions
- describe "GET /api/pleroma/password_reset/token" do
+ describe "GET /api/v1/pleroma/password_reset/token" do
test "it returns error when token invalid", %{conn: conn} do
response =
conn
- |> get("/api/pleroma/password_reset/token")
+ |> get("/api/v1/pleroma/password_reset/token")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
test "it shows password reset form", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
response =
conn
- |> get("/api/pleroma/password_reset/#{token.token}")
+ |> get("/api/v1/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Password Reset for #{user.nickname}</h2>"
end
test "it returns an error when the token has expired", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
response =
conn
- |> get("/api/pleroma/password_reset/#{token.token}")
+ |> get("/api/v1/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
end
- describe "POST /api/pleroma/password_reset" do
+ describe "POST /api/v1/pleroma/password_reset" do
test "it fails for an expired token", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
- |> post("/api/pleroma/password_reset", %{data: params})
+ |> post("/api/v1/pleroma/password_reset", %{data: params})
|> html_response(:ok)
refute response =~ "<h2>Password changed!</h2>"
end
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
- |> post("/api/pleroma/password_reset", %{data: params})
+ |> post("/api/v1/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert response =~ "<h2>Password changed!</h2>"
user = refresh_record(user)
assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
assert Enum.empty?(Token.get_user_tokens(user))
end
test "it sets password_reset_pending to false", %{conn: conn} do
user = insert(:user, password_reset_pending: true)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
conn
|> assign(:user, user)
- |> post("/api/pleroma/password_reset", %{data: params})
+ |> post("/api/v1/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert User.get_by_id(user.id).password_reset_pending == false
end
end
describe "POST /auth/password, with valid parameters" do
setup %{conn: conn} do
user = insert(:user)
conn = post(conn, "/auth/password?email=#{user.email}")
%{conn: conn, user: user}
end
test "it returns 204", %{conn: conn} do
assert empty_json_response(conn)
end
test "it creates a PasswordResetToken record for user", %{user: user} do
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
assert token_record
end
test "it sends an email to user", %{user: user} do
ObanHelpers.perform_all()
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
end
describe "POST /auth/password, with nickname" do
test "it returns 204", %{conn: conn} do
user = insert(:user)
assert conn
|> post("/auth/password?nickname=#{user.nickname}")
|> empty_json_response()
ObanHelpers.perform_all()
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
test "it doesn't fail when a user has no email", %{conn: conn} do
user = insert(:user, %{email: nil})
assert conn
|> post("/auth/password?nickname=#{user.nickname}")
|> empty_json_response()
end
end
describe "POST /auth/password, with invalid parameters" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "it returns 204 when user is not found", %{conn: conn, user: user} do
conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
assert empty_json_response(conn)
end
test "it returns 204 when user is not local", %{conn: conn, user: user} do
{:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false))
conn = post(conn, "/auth/password?email=#{user.email}")
assert empty_json_response(conn)
end
test "it returns 204 when user is deactivated", %{conn: conn, user: user} do
{:ok, user} = Repo.update(Ecto.Changeset.change(user, is_active: false, local: true))
conn = post(conn, "/auth/password?email=#{user.email}")
assert empty_json_response(conn)
end
end
end
diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs
index d669cd0fe..3f839568d 100644
--- a/test/pleroma/web/twitter_api/util_controller_test.exs
+++ b/test/pleroma/web/twitter_api/util_controller_test.exs
@@ -1,982 +1,987 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
import Pleroma.Factory
import Mock
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance])
setup do: clear_config([:frontend_configurations, :pleroma_fe])
describe "PUT /api/pleroma/notification_settings" do
setup do: oauth_access(["write:accounts"])
test "it updates notification settings", %{user: user, conn: conn} do
conn
|> put(
"/api/pleroma/notification_settings?#{URI.encode_query(%{block_from_strangers: true})}"
)
|> json_response_and_validate_schema(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: true,
hide_notification_contents: false
} == user.notification_settings
end
test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do
conn
|> put(
"/api/pleroma/notification_settings?#{URI.encode_query(%{hide_notification_contents: 1})}"
)
|> json_response_and_validate_schema(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: false,
hide_notification_contents: true
} == user.notification_settings
end
end
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
frontend_a: %{
x: 1,
y: 2
},
frontend_b: %{
z: 3
}
]
clear_config(:frontend_configurations, config)
response =
conn
|> get("/api/pleroma/frontend_configurations")
|> json_response_and_validate_schema(:ok)
assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()
end
end
- describe "/api/pleroma/emoji" do
+ describe "/api/v1/pleroma/emoji" do
test "returns json with custom emoji with tags", %{conn: conn} do
emoji =
conn
- |> get("/api/pleroma/emoji")
+ |> get("/api/v1/pleroma/emoji")
|> json_response_and_validate_schema(200)
assert Enum.all?(emoji, fn
{_key,
%{
"image_url" => url,
"tags" => tags
}} ->
is_binary(url) and is_list(tags)
end)
end
end
- describe "GET /api/pleroma/healthcheck" do
+ describe "GET /api/v1/pleroma/healthcheck" do
setup do: clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
clear_config([:instance, :healthcheck], false)
response =
conn
- |> get("/api/pleroma/healthcheck")
+ |> get("/api/v1/pleroma/healthcheck")
|> json_response_and_validate_schema(503)
assert response == %{}
end
test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
clear_config([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
response =
conn
- |> get("/api/pleroma/healthcheck")
+ |> get("/api/v1/pleroma/healthcheck")
|> json_response_and_validate_schema(200)
assert %{
"active" => _,
"healthy" => true,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
clear_config([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do
response =
conn
- |> get("/api/pleroma/healthcheck")
+ |> get("/api/v1/pleroma/healthcheck")
|> json_response_and_validate_schema(503)
assert %{
"active" => _,
"healthy" => false,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
end
describe "POST /api/pleroma/disable_account" do
setup do: oauth_access(["write:accounts"])
test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do
response =
conn
|> post("/api/pleroma/disable_account?password=test")
|> json_response_and_validate_schema(:ok)
assert response == %{"status" => "success"}
ObanHelpers.perform_all()
user = User.get_cached_by_id(user.id)
refute user.is_active
end
test "with valid permissions and invalid password, it returns an error", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/api/pleroma/disable_account?password=test1")
|> json_response_and_validate_schema(:ok)
assert response == %{"error" => "Invalid password."}
user = User.get_cached_by_id(user.id)
assert user.is_active
end
end
describe "POST /main/ostatus - remote_subscribe/2" do
setup do: clear_config([:instance, :federating], true)
test "renders subscribe form", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
|> response(:ok)
refute response =~ "Could not find user"
assert response =~ "Remotely follow #{user.nickname}"
end
test "renders subscribe form with error when user not found", %{conn: conn} do
response =
conn
|> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
|> response(:ok)
assert response =~ "Could not find user"
refute response =~ "Remotely follow"
end
test "it redirect to webfinger url", %{conn: conn} do
user = insert(:user)
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
conn =
conn
|> post("/main/ostatus", %{
"user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
})
assert redirected_to(conn) ==
"https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
end
test "it renders form with error when user not found", %{conn: conn} do
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
response =
conn
|> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
|> response(:ok)
assert response =~ "Something went wrong."
end
end
describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do
setup do: clear_config([:instance, :federating], true)
test "renders subscribe form", %{conn: conn} do
user = insert(:user)
status = insert(:note_activity, %{user: user})
status_id = status.id
assert is_binary(status_id)
response =
conn
|> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""})
|> response(:ok)
refute response =~ "Could not find status"
assert response =~ "Interacting with"
end
test "renders subscribe form with error when status not found", %{conn: conn} do
response =
conn
|> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""})
|> response(:ok)
assert response =~ "Could not find status"
refute response =~ "Interacting with"
end
test "it redirect to webfinger url", %{conn: conn} do
user = insert(:user)
status = insert(:note_activity, %{user: user})
status_id = status.id
status_ap_id = status.data["object"]
assert is_binary(status_id)
assert is_binary(status_ap_id)
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
conn =
conn
|> post("/main/ostatus", %{
"status" => %{"status_id" => status_id, "profile" => user2.ap_id}
})
assert redirected_to(conn) ==
"https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}"
end
test "it renders form with error when status not found", %{conn: conn} do
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
response =
conn
|> post("/main/ostatus", %{
"status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id}
})
|> response(:ok)
assert response =~ "Something went wrong."
end
end
describe "GET /main/ostatus - show_subscribe_form/2" do
setup do: clear_config([:instance, :federating], true)
test "it works with users", %{conn: conn} do
user = insert(:user)
response =
conn
|> get("/main/ostatus", %{"nickname" => user.nickname})
|> response(:ok)
refute response =~ "Could not find user"
assert response =~ "Remotely follow #{user.nickname}"
end
test "it works with statuses", %{conn: conn} do
user = insert(:user)
status = insert(:note_activity, %{user: user})
status_id = status.id
assert is_binary(status_id)
response =
conn
|> get("/main/ostatus", %{"status_id" => status_id})
|> response(:ok)
refute response =~ "Could not find status"
assert response =~ "Interacting with"
end
end
test "it returns new captcha", %{conn: conn} do
with_mock Pleroma.Captcha,
new: fn -> "test_captcha" end do
resp =
conn
- |> get("/api/pleroma/captcha")
+ |> get("/api/v1/pleroma/captcha")
|> response(200)
assert resp == "\"test_captcha\""
assert called(Pleroma.Captcha.new())
end
end
describe "POST /api/pleroma/change_email" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "hi", email: "test@test.com"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "hi", email: "test@test.com"})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and invalid email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: "foobar"})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Email has invalid format."
}
end
test "with proper permissions, valid password and no email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test"})
assert %{"error" => "Missing field: email."} = json_response_and_validate_schema(conn, 400)
end
test "with proper permissions, valid password and blank email, when instance requires user email",
%{
conn: conn
} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], true)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: ""})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with proper permissions, valid password and blank email, when instance does not require user email",
%{
conn: conn
} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], false)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: ""})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password and non unique email", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: user.email})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Email has already been taken."
}
end
test "with proper permissions, valid password and valid email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
- |> post("/api/pleroma/change_email", %{password: "test", email: "cofe@foobar.com"})
+ |> post("/api/pleroma/change_email", %{
+ password: "test",
+ email: "cofe@foobar.com"
+ })
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
end
describe "POST /api/pleroma/change_password" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and new password and confirmation not matching",
%{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with proper permissions, valid password and invalid new password", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
password: "test",
new_password: "",
new_password_confirmation: ""
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with proper permissions, valid password and matching new password and confirmation", %{
conn: conn,
user: user
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/change_password",
%{
password: "test",
new_password: "newpass",
new_password_confirmation: "newpass"
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
fetched_user = User.get_cached_by_id(user.id)
assert Pleroma.Password.Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> post("/api/pleroma/delete_account")
assert json_response_and_validate_schema(conn, 403) ==
%{"error" => "Insufficient permissions: write:accounts."}
end
test "with proper permissions and wrong or missing password", %{conn: conn} do
for params <- [%{"password" => "hi"}, %{}] do
ret_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account", params)
assert json_response_and_validate_schema(ret_conn, 200) == %{
"error" => "Invalid password."
}
end
end
test "with proper permissions and valid password (URL query)", %{conn: conn, user: user} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account?password=test")
ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
user = User.get_by_id(user.id)
refute user.is_active
assert user.name == nil
assert user.bio == ""
assert user.password_hash == nil
end
test "with proper permissions and valid password (JSON body)", %{conn: conn, user: user} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account", %{password: "test"})
ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
user = User.get_by_id(user.id)
refute user.is_active
assert user.name == nil
assert user.bio == ""
assert user.password_hash == nil
end
end
describe "POST /api/pleroma/move_account" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "hi",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "hi",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and target account does not alias this",
%{
conn: conn
} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Target account must have the origin in `alsoKnownAs`"
}
end
test "with proper permissions, valid password and target account does not exist",
%{
conn: conn
} do
target_nick = "not_found@mastodon.social"
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 404) == %{
"error" => "Target account not found."
}
end
test "with proper permissions, valid password, remote target account aliases this and local cache does not exist",
%{} do
user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
%{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
target_nick = "mewmew@lm.kazv.moe"
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password, remote target account aliases this and local cache does not alias this",
%{} do
user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
%{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
target_user =
insert(
:user,
ap_id: "https://lm.kazv.moe/users/mewmew",
nickname: "mewmew@lm.kazv.moe",
local: false
)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password, remote target account does not alias this and local cache aliases this",
%{
user: user,
conn: conn
} do
target_user =
insert(
:user,
ap_id: "https://lm.kazv.moe/users/mewmew",
nickname: "mewmew@lm.kazv.moe",
local: false,
also_known_as: [user.ap_id]
)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Target account must have the origin in `alsoKnownAs`"
}
end
test "with proper permissions, valid password and target account aliases this", %{
conn: conn,
user: user
} do
target_user = insert(:user, also_known_as: [user.ap_id])
target_nick = target_user |> User.full_nickname()
follower = insert(:user)
User.follow(follower, user)
assert User.following?(follower, user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/move_account",
%{
password: "test",
target_account: target_nick
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
params = %{
"op" => "move_following",
"origin_id" => user.id,
"target_id" => target_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
refute User.following?(follower, user)
assert User.following?(follower, target_user)
end
test "prefix nickname by @ should work", %{
conn: conn,
user: user
} do
target_user = insert(:user, also_known_as: [user.ap_id])
target_nick = target_user |> User.full_nickname()
follower = insert(:user)
User.follow(follower, user)
assert User.following?(follower, user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/move_account",
%{
password: "test",
target_account: "@" <> target_nick
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
params = %{
"op" => "move_following",
"origin_id" => user.id,
"target_id" => target_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
refute User.following?(follower, user)
assert User.following?(follower, target_user)
end
end
describe "GET /api/pleroma/aliases" do
setup do: oauth_access(["read:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> get("/api/pleroma/aliases")
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: read:accounts."
}
end
test "with permissions", %{
conn: conn
} do
assert %{"aliases" => []} =
conn
|> get("/api/pleroma/aliases")
|> json_response_and_validate_schema(200)
end
test "with permissions and aliases", %{} do
user = insert(:user)
user2 = insert(:user)
assert {:ok, user} = user |> User.add_alias(user2)
%{user: _user, conn: conn} = oauth_access(["read:accounts"], user: user)
assert %{"aliases" => aliases} =
conn
|> get("/api/pleroma/aliases")
|> json_response_and_validate_schema(200)
assert aliases == [user2 |> User.full_nickname()]
end
end
describe "PUT /api/pleroma/aliases" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{alias: "none"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with permissions, no alias param", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{})
assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
end
test "with permissions, with alias param", %{
conn: conn
} do
user2 = insert(:user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{alias: user2 |> User.full_nickname()})
assert json_response_and_validate_schema(conn, 200) == %{
"status" => "success"
}
end
end
describe "DELETE /api/pleroma/aliases" do
setup do
alias_user = insert(:user)
non_alias_user = insert(:user)
user = insert(:user, also_known_as: [alias_user.ap_id])
oauth_access(["write:accounts"], user: user)
|> Map.put(:alias_user, alias_user)
|> Map.put(:non_alias_user, non_alias_user)
end
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{alias: "none"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with permissions, no alias param", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{})
assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
end
test "with permissions, account does not have such alias", %{
conn: conn,
non_alias_user: non_alias_user
} do
conn =
conn
|> put_req_header("content-type", "application/json")
- |> delete("/api/pleroma/aliases", %{alias: non_alias_user |> User.full_nickname()})
+ |> delete("/api/pleroma/aliases", %{
+ alias: non_alias_user |> User.full_nickname()
+ })
assert %{"error" => "Account has no such alias."} =
json_response_and_validate_schema(conn, 404)
end
test "with permissions, account does have such alias", %{
conn: conn,
alias_user: alias_user
} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{alias: alias_user |> User.full_nickname()})
assert %{"status" => "success"} = json_response_and_validate_schema(conn, 200)
end
end
end
diff --git a/test/support/cluster.ex b/test/support/cluster.ex
index 7e41631c5..682643988 100644
--- a/test/support/cluster.ex
+++ b/test/support/cluster.ex
@@ -1,218 +1,221 @@
defmodule Pleroma.Cluster do
@moduledoc """
Facilities for managing a cluster of slave VM's for federated testing.
## Spawning the federated cluster
`spawn_cluster/1` spawns a map of slave nodes that are started
within the running VM. During startup, the slave node is sent all configuration
from the parent node, as well as all code. After receiving configuration and
code, the slave then starts all applications currently running on the parent.
The configuration passed to `spawn_cluster/1` overrides any parent application
configuration for the provided OTP app and key. This is useful for customizing
the Ecto database, Phoenix webserver ports, etc.
For example, to start a single federated VM named ":federated1", with the
Pleroma Endpoint running on port 4123, and with a database named
"pleroma_test1", you would run:
endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
Pleroma.Cluster.spawn_cluster(%{
:"federated1@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
]
})
*Note*: application configuration for a given key is not merged,
so any customization requires first fetching the existing values
and merging yourself by providing the merged configuration,
such as above with the endpoint config and repo config.
## Executing code within a remote node
Use the `within/2` macro to execute code within the context of a remote
federated node. The code block captures all local variable bindings from
the parent's context and returns the result of the expression after executing
it on the remote node. For example:
import Pleroma.Cluster
parent_value = 123
result =
within :"federated1@127.0.0.1" do
{node(), parent_value}
end
assert result == {:"federated1@127.0.0.1, 123}
*Note*: while local bindings are captured and available within the block,
other parent contexts like required, aliased, or imported modules are not
in scope. Those will need to be reimported/aliases/required within the block
as `within/2` is a remote procedure call.
"""
@extra_apps Pleroma.Mixfile.application()[:extra_applications]
@doc """
Spawns the default Pleroma federated cluster.
Values before may be customized as needed for the test suite.
"""
def spawn_default_cluster do
endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
spawn_cluster(%{
:"federated1@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
],
:"federated2@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
]
})
end
@doc """
Spawns a configured map of federated nodes.
See `Pleroma.Cluster` module documentation for details.
"""
def spawn_cluster(node_configs) do
# Turn node into a distributed node with the given long name
:net_kernel.start([:"primary@127.0.0.1"])
# Allow spawned nodes to fetch all code from this node
{:ok, _} = :erl_boot_server.start([])
allow_boot("127.0.0.1")
silence_logger_warnings(fn ->
node_configs
|> Enum.map(&Task.async(fn -> start_peer(&1) end))
|> Enum.map(&Task.await(&1, 90_000))
end)
end
@doc """
Executes block of code again remote node.
See `Pleroma.Cluster` module documentation for details.
"""
defmacro within(node, do: block) do
quote do
rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
unquote(Macro.escape(block)),
binding()
])
end
end
@doc false
def eval_quoted(block, binding) do
{result, _binding} = Code.eval_quoted(block, binding, __ENV__)
result
end
defp start_peer({node_host, override_configs}) do
log(node_host, "booting federated VM")
- {:ok, node} = :peer.start(%{host: ~c"127.0.0.1", name: node_name(node_host), args: vm_args()})
+
+ {:ok, _pid, node} =
+ :peer.start(%{host: ~c"127.0.0.1", name: node_name(node_host), args: vm_args()})
+
add_code_paths(node)
load_apps_and_transfer_configuration(node, override_configs)
ensure_apps_started(node)
{:ok, node}
end
def rpc(node, module, function, args) do
:rpc.block_call(node, module, function, args)
end
defp vm_args do
~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
end
defp allow_boot(host) do
{:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
:ok = :erl_boot_server.add_slave(ipv4)
end
defp add_code_paths(node) do
rpc(node, :code, :add_paths, [:code.get_path()])
end
defp load_apps_and_transfer_configuration(node, override_configs) do
Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
app_name
|> Application.get_all_env()
|> Enum.each(fn {key, primary_config} ->
rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
end)
end)
Enum.each(override_configs, fn {app_name, key, val} ->
rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
end)
end
defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
defp ensure_apps_started(node) do
loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
app_names = @extra_apps ++ (loaded_names -- @extra_apps)
rpc(node, Application, :ensure_all_started, [:mix])
rpc(node, Mix, :env, [Mix.env()])
rpc(node, __MODULE__, :prepare_database, [])
log(node, "starting application")
Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
if Enum.member?(loaded, app) do
loaded
else
{:ok, started} = rpc(node, Application, :ensure_all_started, [app])
MapSet.union(loaded, MapSet.new(started))
end
end)
end
@doc false
def prepare_database do
log(node(), "preparing database")
repo_config = Application.get_env(:pleroma, Pleroma.Repo)
repo_config[:adapter].storage_down(repo_config)
repo_config[:adapter].storage_up(repo_config)
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
Ecto.Migrator.run(repo, :up, log: false, all: true)
end)
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
{:ok, _} = Application.ensure_all_started(:ex_machina)
end
defp silence_logger_warnings(func) do
prev_level = Logger.level()
Logger.configure(level: :error)
res = func.()
Logger.configure(level: prev_level)
res
end
defp node_name(node_host) do
node_host
|> to_string()
|> String.split("@")
|> Enum.at(0)
|> String.to_atom()
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 8, 4:00 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1723671
Default Alt Text
(961 KB)

Event Timeline