diff --git a/.formatter.exs b/.formatter.exs index 7fa95a619..5799ac127 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,3 @@ [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs"] + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/scrubbers/*.ex"] ] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab62c8827..88789035d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,7 @@ build: benchmark: stage: benchmark + when: manual variables: MIX_ENV: benchmark services: @@ -55,6 +56,19 @@ unit-testing: - mix ecto.migrate - mix coveralls --preload-modules +federated-testing: + stage: test + services: + - name: minibikini/postgres-with-rum:12 + alias: postgres + command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] + script: + - mix deps.get + - mix ecto.create + - mix ecto.migrate + - epmd -daemon + - mix test --trace --only federated + unit-testing-rum: stage: test services: diff --git a/CHANGELOG.md b/CHANGELOG.md index e44c892ab..56b6371d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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`).
API Changes @@ -35,9 +36,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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).
### Added +- `:chat_limit` option to limit chat characters. - 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. @@ -45,6 +49,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix task to list all users (`mix pleroma.user list`) - 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. +- User settings: Add _This account is a_ option. +- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
API Changes @@ -66,22 +73,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `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: `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.
### 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
API Changes - 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
diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index cdc073b2e..a45a71d4a 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -95,7 +95,36 @@ def query_timelines(user) do for: user, as: :activity }) - end + end, + "Rendering favorites timeline" => fn -> + conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil) + Pleroma.Web.MastodonAPI.StatusController.favourites( + %Plug.Conn{conn | + assigns: %{user: user}, + query_params: %{"limit" => "0"}, + body_params: %{}, + cookies: %{}, + params: %{}, + path_params: %{}, + private: %{ + Pleroma.Web.Router => {[], %{}}, + phoenix_router: Pleroma.Web.Router, + phoenix_action: :favourites, + phoenix_controller: Pleroma.Web.MastodonAPI.StatusController, + phoenix_endpoint: Pleroma.Web.Endpoint, + phoenix_format: "json", + phoenix_layout: {Pleroma.Web.LayoutView, "app.html"}, + phoenix_recycled: true, + + phoenix_view: Pleroma.Web.MastodonAPI.StatusView, + plug_session: %{"user_id" => user.id}, + plug_session_fetch: :done, + plug_session_info: :write, + plug_skip_csrf_protection: true + } + }, + %{}) + end, }) end diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index b4432bdb7..a957e0ffb 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -2,6 +2,24 @@ defmodule Pleroma.LoadTesting.Generator do use Pleroma.LoadTesting.Helper alias Pleroma.Web.CommonAPI + def generate_like_activities(user, posts) do + count_likes = Kernel.trunc(length(posts) / 4) + IO.puts("Starting generating #{count_likes} like activities...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + Enum.take_random(posts, count_likes), + fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting like activities take #{to_sec(time)} sec.\n") + end + def generate_users(opts) do IO.puts("Starting generating #{opts[:users_max]} users...") {time, _} = :timer.tc(fn -> do_generate_users(opts) end) @@ -31,7 +49,6 @@ defp generate_user_data(i) do password_hash: "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg", bio: "Tester Number #{i}", - info: %{}, local: remote } diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex index 4fa3eec49..0a751adac 100644 --- a/benchmarks/mix/tasks/pleroma/load_testing.ex +++ b/benchmarks/mix/tasks/pleroma/load_testing.ex @@ -100,6 +100,10 @@ def run(args) do generate_remote_activities(user, remote_users) + generate_like_activities( + user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create")) + ) + generate_dms(user, users, opts) {:ok, activity} = generate_long_thread(user, users, opts) diff --git a/config/config.exs b/config/config.exs index b60ffef7d..6ed800056 100644 --- a/config/config.exs +++ b/config/config.exs @@ -225,6 +225,7 @@ notify_email: "noreply@example.com", description: "A Pleroma instance, an alternative fediverse server", limit: 5_000, + chat_limit: 5_000, remote_limit: 100_000, upload_limit: 16_000_000, avatar_upload_limit: 2_000_000, @@ -562,7 +563,10 @@ base_path: "/oauth", providers: ueberauth_providers -config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies +config :pleroma, + :auth, + enforce_oauth_admin_scope_usage: false, + oauth_consumer_strategies: oauth_consumer_strategies config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false diff --git a/config/description.exs b/config/description.exs index 70e963399..45e4b43f1 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2094,6 +2094,15 @@ type: :group, description: "Authentication / authorization settings", children: [ + %{ + key: :enforce_oauth_admin_scope_usage, + type: :boolean, + description: + "OAuth admin scope requirement toggle. " <> + "If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <> + "(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <> + "`is_admin` user flag grants access to admin-specific actions." + }, %{ key: :auth_template, type: :string, diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 2cac317de..b19793150 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -2,6 +2,13 @@ Authentication is required and the user must be an admin. +Configuration options: + +* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle. + If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). + If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions. + Note that client app needs to explicitly support admin scopes and request them when obtaining auth token. + ## `GET /api/pleroma/admin/users` ### List users diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 7fbe17130..7f5d7681d 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -57,6 +57,7 @@ Has these additional fields under the `pleroma` object: - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `deactivated`: boolean, true when the user is deactivated +- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. ### Source @@ -65,6 +66,8 @@ Has these additional fields under the `pleroma` object: - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API +- `discoverable`: boolean, true when the user allows discovery of the account in search results and other services. +- `actor_type`: string, the type of this account. ## Conversations @@ -91,11 +94,18 @@ Has these additional fields under the `pleroma` object: - `is_seen`: true if the notification was read by the user +### Move Notification + +The `type` value is `move`. Has an additional field: + +- `target`: new account + ## GET `/api/v1/notifications` Accepts additional parameters: - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. +- `with_move`: boolean, when set to `true` will include Move notifications. `false` by default. ## POST `/api/v1/statuses` @@ -136,7 +146,10 @@ Additional parameters can be added to the JSON body/Form data: - `default_scope` - the scope returned under `privacy` key in Source subentity - `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `skip_thread_containment` - if true, skip filtering out broken threads +- `allow_following_move` - if true, allows automatically follow moved following accounts - `pleroma_background_image` - sets the background image of the user. +- `discoverable` - if true, discovery of this account in search results and other services is allowed. +- `actor_type` - the type of this account. ### Pleroma Settings Store Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index ad16d027e..7228d805b 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -302,6 +302,7 @@ See [Admin-API](admin_api.md) * `follows`: BOOLEAN field, receives notifications from people the user follows * `remote`: BOOLEAN field, receives notifications from people on remote instances * `local`: BOOLEAN field, receives notifications from people on the local instance + * `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification. * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` ## `/api/pleroma/healthcheck` diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md index ce19e2402..e9d44b9a4 100644 --- a/docs/administration/CLI_tasks/config.md +++ b/docs/administration/CLI_tasks/config.md @@ -3,17 +3,26 @@ !!! danger This is a Work In Progress, not usable just yet. -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl config` and in case of source installs it's -`mix pleroma.config`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Transfer config from file to DB. -```sh -$PREFIX migrate_to_db +```sh tab="OTP" + ./bin/pleroma_ctl config migrate_to_db ``` +```sh tab="From Source" +mix pleroma.config migrate_to_db +``` + + ## Transfer config from DB to `config/env.exported_from_db.secret.exs` -```sh -$PREFIX migrate_from_db +```sh tab="OTP" + ./bin/pleroma_ctl config migrate_from_db ``` + +```sh tab="From Source" +mix pleroma.config migrate_from_db +``` + diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md index 3011646c8..51c7484ba 100644 --- a/docs/administration/CLI_tasks/database.md +++ b/docs/administration/CLI_tasks/database.md @@ -1,6 +1,6 @@ # Database maintenance tasks -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} !!! danger These mix tasks can take a long time to complete. Many of them were written to address specific database issues that happened because of bugs in migrations or other specific scenarios. Do not run these tasks "just in case" if everything is fine your instance. @@ -9,8 +9,12 @@ Every command should be ran with a prefix, in case of OTP releases it is `./bin/ Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. -```sh -$PREFIX remove_embedded_objects [] +```sh tab="OTP" +./bin/pleroma_ctl database remove_embedded_objects [] +``` + +```sh tab="From Source" +mix pleroma.database remove_embedded_objects [] ``` ### Options @@ -20,11 +24,15 @@ $PREFIX remove_embedded_objects [] This will prune remote posts older than 90 days (configurable with [`config :pleroma, :instance, remote_post_retention_days`](../../configuration/cheatsheet.md#instance)) from the database, they will be refetched from source when accessed. -!!! note - The disk space will only be reclaimed after `VACUUM FULL` +!!! danger + The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free. -```sh -$PREFIX pleroma.database prune_objects [] +```sh tab="OTP" +./bin/pleroma_ctl database prune_objects [] +``` + +```sh tab="From Source" +mix pleroma.database prune_objects [] ``` ### Options @@ -34,18 +42,30 @@ $PREFIX pleroma.database prune_objects [] Can be safely re-run -```sh -$PREFIX bump_all_conversations +```sh tab="OTP" +./bin/pleroma_ctl database bump_all_conversations +``` + +```sh tab="From Source" +mix pleroma.database bump_all_conversations ``` ## Remove duplicated items from following and update followers count for all users -```sh -$PREFIX update_users_following_followers_counts +```sh tab="OTP" +./bin/pleroma_ctl database update_users_following_followers_counts +``` + +```sh tab="From Source" +mix pleroma.database update_users_following_followers_counts ``` ## Fix the pre-existing "likes" collections for all objects -```sh -$PREFIX fix_likes_collections +```sh tab="OTP" +./bin/pleroma_ctl database fix_likes_collections +``` + +```sh tab="From Source" +mix pleroma.database fix_likes_collections ``` diff --git a/docs/administration/CLI_tasks/digest.md b/docs/administration/CLI_tasks/digest.md index 547702031..a70f24c06 100644 --- a/docs/administration/CLI_tasks/digest.md +++ b/docs/administration/CLI_tasks/digest.md @@ -1,13 +1,24 @@ # Managing digest emails -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl digest` and in case of source installs it's `mix pleroma.digest`. + +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Send digest email since given date (user registration date by default) ignoring user activity status. -```sh -$PREFIX test [] +```sh tab="OTP" + ./bin/pleroma_ctl digest test [] ``` -Example: -```sh -$PREFIX test donaldtheduck 2019-05-20 +```sh tab="From Source" +mix pleroma.digest test [] ``` + + +Example: +```sh tab="OTP" + ./bin/pleroma_ctl digest test donaldtheduck 2019-05-20 +``` + +```sh tab="From Source" +mix pleroma.digest test donaldtheduck 2019-05-20 +``` + diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md index eee02f2ef..a3207bc6c 100644 --- a/docs/administration/CLI_tasks/emoji.md +++ b/docs/administration/CLI_tasks/emoji.md @@ -1,28 +1,44 @@ # Managing emoji packs -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl emoji` and in case of source installs it's `mix pleroma.emoji`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Lists emoji packs and metadata specified in the manifest -```sh -$PREFIX ls-packs [] +```sh tab="OTP" +./bin/pleroma_ctl emoji ls-packs [] ``` +```sh tab="From Source" +mix pleroma.emoji ls-packs [] +``` + + ### Options - `-m, --manifest PATH/URL` - path to a custom manifest, it can either be an URL starting with `http`, in that case the manifest will be fetched from that address, or a local path ## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME` -```sh -$PREFIX get-packs [] + +```sh tab="OTP" +./bin/pleroma_ctl emoji get-packs [] +``` + +```sh tab="From Source" +mix pleroma.emoji get-packs [] ``` ### Options - `-m, --manifest PATH/URL` - same as [`ls-packs`](#ls-packs) ## Create a new manifest entry and a file list from the specified remote pack file -```sh -$PREFIX gen-pack PACK-URL + +```sh tab="OTP" +./bin/pleroma_ctl emoji gen-pack PACK-URL ``` + +```sh tab="From Source" +mix pleroma.emoji gen-pack PACK-URL +``` + Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you. The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. diff --git a/docs/administration/CLI_tasks/general_cli_task_info.include b/docs/administration/CLI_tasks/general_cli_task_info.include new file mode 100644 index 000000000..a1ff1da12 --- /dev/null +++ b/docs/administration/CLI_tasks/general_cli_task_info.include @@ -0,0 +1,5 @@ +Every command should be ran as the `pleroma` user from it's home directory. For example if you are superuser, you would have to wrap the command in `su pleroma -s $SHELL -lc "$COMMAND"`. + +??? note "From source note about `MIX_ENV`" + + The `mix` command should be prefixed with the name of environment your Pleroma server is running in, usually it's `MIX_ENV=prod` diff --git a/docs/administration/CLI_tasks/instance.md b/docs/administration/CLI_tasks/instance.md index ab0b68ad0..1a3b268be 100644 --- a/docs/administration/CLI_tasks/instance.md +++ b/docs/administration/CLI_tasks/instance.md @@ -1,12 +1,17 @@ # Managing instance configuration -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl instance` and in case of source installs it's `mix pleroma.instance`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Generate a new configuration file -```sh -$PREFIX gen [] +```sh tab="OTP" + ./bin/pleroma_ctl instance gen [] ``` +```sh tab="From Source" +mix pleroma.instance gen [] +``` + + If any of the options are left unspecified, you will be prompted interactively. ### Options diff --git a/docs/administration/CLI_tasks/relay.md b/docs/administration/CLI_tasks/relay.md index aa44617df..c4f078f4d 100644 --- a/docs/administration/CLI_tasks/relay.md +++ b/docs/administration/CLI_tasks/relay.md @@ -1,30 +1,33 @@ # Managing relays -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl relay` and in case of source installs it's `mix pleroma.relay`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Follow a relay -```sh -$PREFIX follow + +```sh tab="OTP" +./bin/pleroma_ctl relay follow ``` -Example: -```sh -$PREFIX follow https://example.org/relay +```sh tab="From Source" +mix pleroma.relay follow ``` ## Unfollow a remote relay -```sh -$PREFIX unfollow +```sh tab="OTP" +./bin/pleroma_ctl relay unfollow ``` -Example: -```sh -$PREFIX unfollow https://example.org/relay +```sh tab="From Source" +mix pleroma.relay unfollow ``` ## List relay subscriptions -```sh -$PREFIX list +```sh tab="OTP" +./bin/pleroma_ctl relay list +``` + +```sh tab="From Source" +mix pleroma.relay list ``` diff --git a/docs/administration/CLI_tasks/uploads.md b/docs/administration/CLI_tasks/uploads.md index 71800e341..e36c94c38 100644 --- a/docs/administration/CLI_tasks/uploads.md +++ b/docs/administration/CLI_tasks/uploads.md @@ -1,11 +1,16 @@ # Managing uploads -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl uploads` and in case of source installs it's `mix pleroma.uploads`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Migrate uploads from local to remote storage -```sh -$PREFIX migrate_local [] +```sh tab="OTP" + ./bin/pleroma_ctl uploads migrate_local [] ``` + +```sh tab="From Source" +mix pleroma.uploads migrate_local [] +``` + ### Options - `--delete` - delete local uploads after migrating them to the target uploader diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 96b2d9e6a..da8363131 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -1,12 +1,18 @@ # Managing users -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl user` and in case of source installs it's `mix pleroma.user`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Create a user -```sh -$PREFIX new [] + +```sh tab="OTP" +./bin/pleroma_ctl user new [] ``` +```sh tab="From Source" +mix pleroma.user new [] +``` + + ### Options - `--name ` - the user's display name - `--bio ` - the user's bio @@ -16,84 +22,159 @@ $PREFIX new [] - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions ## List local users -```sh -$PREFIX list +```sh tab="OTP" + ./bin/pleroma_ctl user list ``` -## Generate an invite link -```sh -$PREFIX invite [] +```sh tab="From Source" +mix pleroma.user list ``` + +## Generate an invite link +```sh tab="OTP" + ./bin/pleroma_ctl user invite [] +``` + +```sh tab="From Source" +mix pleroma.user invite [] +``` + + ### Options - `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05") - `--max-use NUMBER` - maximum numbers of token uses ## List generated invites -```sh -$PREFIX invites +```sh tab="OTP" + ./bin/pleroma_ctl user invites ``` +```sh tab="From Source" +mix pleroma.user invites +``` + + ## Revoke invite -```sh -$PREFIX revoke_invite +```sh tab="OTP" + ./bin/pleroma_ctl user revoke_invite ``` +```sh tab="From Source" +mix pleroma.user revoke_invite +``` + + ## Delete a user -```sh -$PREFIX rm +```sh tab="OTP" + ./bin/pleroma_ctl user rm ``` +```sh tab="From Source" +mix pleroma.user rm +``` + + ## Delete user's posts and interactions -```sh -$PREFIX delete_activities +```sh tab="OTP" + ./bin/pleroma_ctl user delete_activities ``` +```sh tab="From Source" +mix pleroma.user delete_activities +``` + + ## Sign user out from all applications (delete user's OAuth tokens and authorizations) -```sh -$PREFIX sign_out +```sh tab="OTP" + ./bin/pleroma_ctl user sign_out ``` +```sh tab="From Source" +mix pleroma.user sign_out +``` + + ## Deactivate or activate a user -```sh -$PREFIX toggle_activated +```sh tab="OTP" + ./bin/pleroma_ctl user toggle_activated ``` +```sh tab="From Source" +mix pleroma.user toggle_activated +``` + + ## Unsubscribe local users from a user and deactivate the user -```sh -$PREFIX unsubscribe NICKNAME +```sh tab="OTP" + ./bin/pleroma_ctl user unsubscribe NICKNAME ``` +```sh tab="From Source" +mix pleroma.user unsubscribe NICKNAME +``` + + ## Unsubscribe local users from an instance and deactivate all accounts on it -```sh -$PREFIX unsubscribe_all_from_instance +```sh tab="OTP" + ./bin/pleroma_ctl user unsubscribe_all_from_instance ``` +```sh tab="From Source" +mix pleroma.user unsubscribe_all_from_instance +``` + + ## Create a password reset link for user -```sh -$PREFIX reset_password +```sh tab="OTP" + ./bin/pleroma_ctl user reset_password ``` -## Set the value of the given user's settings -```sh -$PREFIX set [] +```sh tab="From Source" +mix pleroma.user reset_password ``` + + +## Set the value of the given user's settings +```sh tab="OTP" + ./bin/pleroma_ctl user set [] +``` + +```sh tab="From Source" +mix pleroma.user set [] +``` + ### Options - `--locked`/`--no-locked` - whether the user should be locked - `--moderator`/`--no-moderator` - whether the user should be a moderator - `--admin`/`--no-admin` - whether the user should be an admin ## Add tags to a user -```sh -$PREFIX tag +```sh tab="OTP" + ./bin/pleroma_ctl user tag ``` +```sh tab="From Source" +mix pleroma.user tag +``` + + ## Delete tags from a user -```sh -$PREFIX untag +```sh tab="OTP" + ./bin/pleroma_ctl user untag ``` -## Toggle confirmation status of the user -```sh -$PREFIX toggle_confirmed +```sh tab="From Source" +mix pleroma.user untag ``` + + +## Toggle confirmation status of the user +```sh tab="OTP" + ./bin/pleroma_ctl user toggle_confirmed +``` + +```sh tab="From Source" +mix pleroma.user toggle_confirmed +``` + diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index dc2f55229..ef2711e3c 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -12,6 +12,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `notify_email`: Email used for notifications. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). +* `chat_limit`: Character limit of the instance chat messages. * `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `upload_limit`: File size limit of uploads (except for avatar, background, banner). * `avatar_upload_limit`: File size limit of user’s profile avatars. diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 0e21408b2..590c7a914 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,7 +52,9 @@ def run(["migrate_from_db", env, delete?]) do |> Enum.each(fn config -> IO.write( file, - "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" + "config :#{config.group}, #{config.key}, #{ + inspect(Config.from_binary(config.value), limit: :infinity) + }\r\n\r\n" ) if delete? do diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex new file mode 100644 index 000000000..7d65f0587 --- /dev/null +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -0,0 +1,83 @@ +defmodule Mix.Tasks.Pleroma.NotificationSettings do + @shortdoc "Enable&Disable privacy option for push notifications" + @moduledoc """ + Example: + + > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user + > mix pleroma.notification_settings --privacy-option=true # set true for all users + + """ + + use Mix.Task + import Mix.Pleroma + import Ecto.Query + + def run(args) do + start_pleroma() + + {options, _, _} = + OptionParser.parse( + args, + strict: [ + privacy_option: :boolean, + email_users: :string, + nickname_users: :string + ] + ) + + privacy_option = Keyword.get(options, :privacy_option) + + if not is_nil(privacy_option) do + privacy_option + |> build_query(options) + |> Pleroma.Repo.update_all([]) + end + + shell_info("Done") + end + + defp build_query(privacy_option, options) do + query = + from(u in Pleroma.User, + update: [ + set: [ + notification_settings: + fragment( + "jsonb_set(notification_settings, '{privacy_option}', ?)", + ^privacy_option + ) + ] + ] + ) + + user_emails = + options + |> Keyword.get(:email_users, "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.reject(&(&1 == "")) + + query = + if length(user_emails) > 0 do + where(query, [u], u.email in ^user_emails) + else + query + end + + user_nicknames = + options + |> Keyword.get(:nickname_users, "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.reject(&(&1 == "")) + + query = + if length(user_nicknames) > 0 do + where(query, [u], u.nickname in ^user_nicknames) + else + query + end + + query + end +end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bc8eacda8..85c9e4954 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do alias Ecto.Changeset alias Pleroma.User alias Pleroma.UserInviteToken - alias Pleroma.Web.OAuth @shortdoc "Manages Pleroma users" @moduledoc File.read!("docs/administration/CLI_tasks/user.md") @@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - OAuth.Token.delete_user_tokens(user) - OAuth.Authorization.delete_user_authorizations(user) + User.global_sign_out(user) shell_info("#{nickname} signed out from all apps.") else @@ -373,9 +371,9 @@ def run(["list"]) do users |> Enum.each(fn user -> shell_info( - "#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{ - user.info.locked - }, deactivated: #{user.info.deactivated}" + "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{ + user.locked + }, deactivated: #{user.deactivated}" ) end) end) @@ -393,10 +391,7 @@ defp set_moderator(user, value) do end defp set_admin(user, value) do - {:ok, user} = - user - |> Changeset.change(%{is_admin: value}) - |> User.update_and_set_cache() + {:ok, user} = User.admin_api_update(user, %{is_admin: value}) shell_info("Admin status of #{user.nickname}: #{user.is_admin}") user diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index cd7a5aae9..480b261cf 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -28,7 +28,8 @@ defmodule Pleroma.Activity do "Create" => "mention", "Follow" => "follow", "Announce" => "reblog", - "Like" => "favourite" + "Like" => "favourite", + "Move" => "move" } @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types, @@ -240,9 +241,10 @@ def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) def normalize(_), do: nil - def delete_by_ap_id(id) when is_binary(id) do + def delete_all_by_object_ap_id(id) when is_binary(id) do id |> Queries.by_object_id() + |> Queries.exclude_type("Delete") |> select([u], u) |> Repo.delete_all() |> elem(1) @@ -254,7 +256,7 @@ def delete_by_ap_id(id) when is_binary(id) do |> purge_web_resp_cache() end - def delete_by_ap_id(_), do: nil + def delete_all_by_object_ap_id(_), do: nil defp purge_web_resp_cache(%Activity{} = activity) do %{path: path} = URI.parse(activity.data["id"]) diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index 949f010a8..26bc1099d 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -64,4 +64,12 @@ def by_type(query \\ Activity, activity_type) do where: fragment("(?)->>'type' = ?", activity.data, ^activity_type) ) end + + @spec exclude_type(query, String.t()) :: query + def exclude_type(query \\ Activity, activity_type) do + from( + activity in query, + where: fragment("(?)->>'type' != ?", activity.data, ^activity_type) + ) + end end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index f847ac238..d30a5a6a5 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -86,7 +86,7 @@ defp maybe_fetch(activities, user, search_query) do {:ok, object} <- Fetcher.fetch_object_from_id(search_query), %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user) do - activities ++ [activity] + [activity | activities] else _ -> activities end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9dbd1e26b..5b844aa41 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -30,6 +30,7 @@ def user_agent do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do + Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() setup_instrumenters() @@ -147,8 +148,6 @@ defp oauth_cleanup_child(true), defp oauth_cleanup_child(_), do: [] - defp chat_child(:test, _), do: [] - defp chat_child(_env, true) do [Pleroma.Web.ChatChannel.ChatChannelState] end diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex index bd20952a6..6e6121d4e 100644 --- a/lib/pleroma/clippy.ex +++ b/lib/pleroma/clippy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Clippy do @moduledoc false + # No software is complete until they have a Clippy implementation. # A ballmer peak _may_ be required to change this module. diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index fcc039710..cadab2f15 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -65,4 +65,11 @@ def delete(key) do def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] + + def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage]) + + def oauth_admin_scopes(scope) do + ["admin:#{scope}"] ++ + if enforce_oauth_admin_scope_usage?(), do: [], else: [scope] + end end diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex new file mode 100644 index 000000000..b86229312 --- /dev/null +++ b/lib/pleroma/ecto_enums.ex @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +import EctoEnum + +defenum(UserRelationshipTypeEnum, + block: 1, + mute: 2, + reblog_mute: 3, + notification_mute: 4, + inverse_subscription: 5 +) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3aff9fb76..0b0219b82 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -107,4 +107,26 @@ def following(%User{} = user) do [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) + |> limit(50) + |> preload([:follower]) + |> Repo.all() + |> Enum.map(fn following_relationship -> + Repo.delete(following_relationship) + Pleroma.Web.CommonAPI.follow(following_relationship.follower, target) + end) + |> case do + [] -> + User.update_follower_count(origin) + :ok + + _ -> + move_following(origin, target) + end + end end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 997e965f0..2cae29f35 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -3,6 +3,25 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTML do + # Scrubbers are compiled on boot so they can be configured in OTP releases + # @on_load :compile_scrubbers + + def compile_scrubbers do + dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") + + dir + |> File.ls!() + |> Enum.map(&Path.join(dir, &1)) + |> Kernel.ParallelCompiler.compile() + |> case do + {:error, _errors, _warnings} -> + raise "Compiling scrubbers failed" + + {:ok, _modules, _warnings} -> + :ok + end + end + defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber] defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] @@ -99,215 +118,3 @@ def extract_first_external_url(object, content) do end) end end - -defmodule Pleroma.HTML.Scrubber.TwitterText do - @moduledoc """ - An HTML scrubbing policy which limits to twitter-style text. Only - paragraphs, breaks and links are allowed through the filter. - """ - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - - Meta.strip_comments() - - # links - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "class", [ - "hashtag", - "u-url", - "mention", - "u-url mention", - "mention u-url" - ]) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - - # paragraphs and linebreaks - Meta.allow_tag_with_these_attributes(:br, []) - Meta.allow_tag_with_these_attributes(:p, []) - - # microformats - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) - Meta.allow_tag_with_these_attributes(:span, []) - - # allow inline images for custom emoji - if Pleroma.Config.get([:markup, :allow_inline_images]) do - # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - - Meta.allow_tag_with_these_attributes(:img, [ - "width", - "height", - "class", - "title", - "alt" - ]) - end - - Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Scrubber.Default do - @doc "The default HTML scrubbing policy: no " - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - # credo:disable-for-previous-line - # No idea how to fix this one… - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - Meta.strip_comments() - - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "class", [ - "hashtag", - "u-url", - "mention", - "u-url mention", - "mention u-url" - ]) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer", - "ugc" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - - Meta.allow_tag_with_these_attributes(:abbr, ["title"]) - - Meta.allow_tag_with_these_attributes(:b, []) - Meta.allow_tag_with_these_attributes(:blockquote, []) - Meta.allow_tag_with_these_attributes(:br, []) - Meta.allow_tag_with_these_attributes(:code, []) - Meta.allow_tag_with_these_attributes(:del, []) - Meta.allow_tag_with_these_attributes(:em, []) - Meta.allow_tag_with_these_attributes(:i, []) - Meta.allow_tag_with_these_attributes(:li, []) - Meta.allow_tag_with_these_attributes(:ol, []) - Meta.allow_tag_with_these_attributes(:p, []) - Meta.allow_tag_with_these_attributes(:pre, []) - Meta.allow_tag_with_these_attributes(:strong, []) - Meta.allow_tag_with_these_attributes(:sub, []) - Meta.allow_tag_with_these_attributes(:sup, []) - Meta.allow_tag_with_these_attributes(:u, []) - Meta.allow_tag_with_these_attributes(:ul, []) - - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) - Meta.allow_tag_with_these_attributes(:span, []) - - @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) - - if @allow_inline_images do - # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - - Meta.allow_tag_with_these_attributes(:img, [ - "width", - "height", - "class", - "title", - "alt" - ]) - end - - if Pleroma.Config.get([:markup, :allow_tables]) do - Meta.allow_tag_with_these_attributes(:table, []) - Meta.allow_tag_with_these_attributes(:tbody, []) - Meta.allow_tag_with_these_attributes(:td, []) - Meta.allow_tag_with_these_attributes(:th, []) - Meta.allow_tag_with_these_attributes(:thead, []) - Meta.allow_tag_with_these_attributes(:tr, []) - end - - if Pleroma.Config.get([:markup, :allow_headings]) do - Meta.allow_tag_with_these_attributes(:h1, []) - Meta.allow_tag_with_these_attributes(:h2, []) - Meta.allow_tag_with_these_attributes(:h3, []) - Meta.allow_tag_with_these_attributes(:h4, []) - Meta.allow_tag_with_these_attributes(:h5, []) - end - - if Pleroma.Config.get([:markup, :allow_fonts]) do - Meta.allow_tag_with_these_attributes(:font, ["face"]) - end - - Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Transform.MediaProxy do - @moduledoc "Transforms inline image URIs to use MediaProxy." - - alias Pleroma.Web.MediaProxy - - def before_scrub(html), do: html - - def scrub_attribute(:img, {"src", "http" <> target}) do - media_url = - ("http" <> target) - |> MediaProxy.url() - - {"src", media_url} - end - - def scrub_attribute(_tag, attribute), do: attribute - - def scrub({:img, attributes, children}) do - attributes = - attributes - |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) - |> Enum.reject(&is_nil(&1)) - - {:img, attributes, children} - end - - def scrub({:comment, _text, _children}), do: "" - - def scrub({tag, attributes, children}), do: {tag, attributes, children} - def scrub({_tag, children}), do: children - def scrub(text), do: text -end - -defmodule Pleroma.HTML.Scrubber.LinksOnly do - @moduledoc """ - An HTML scrubbing policy which limits to links only. - """ - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - - Meta.strip_comments() - - # links - Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer", - "me", - "ugc" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - Meta.strip_everything_not_covered() -end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..8f3e46af9 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -21,6 +21,8 @@ defmodule Pleroma.Notification do @type t :: %__MODULE__{} + @include_muted_option :with_muted + schema "notifications" do field(:seen, :boolean, default: false) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) @@ -34,7 +36,25 @@ def changeset(%Notification{} = notification, attrs) do |> cast(attrs, [:seen]) end - def for_user_query(user, opts \\ []) do + defp for_user_query_ap_id_opts(user, opts) do + ap_id_relations = + [:block] ++ + if opts[@include_muted_option], do: [], else: [:notification_mute] + + preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) + + exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) + + exclude_notification_muted_opts = + Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts) + + {exclude_blocked_opts, exclude_notification_muted_opts} + end + + def for_user_query(user, opts \\ %{}) do + {exclude_blocked_opts, exclude_notification_muted_opts} = + for_user_query_ap_id_opts(user, opts) + Notification |> where(user_id: ^user.id) |> where( @@ -54,43 +74,75 @@ def for_user_query(user, opts \\ []) do ) ) |> preload([n, a, o], activity: {a, object: o}) - |> exclude_muted(user, opts) - |> exclude_blocked(user) + |> exclude_notification_muted(user, exclude_notification_muted_opts) + |> exclude_blocked(user, exclude_blocked_opts) |> exclude_visibility(opts) + |> exclude_move(opts) end - defp exclude_blocked(query, user) do + defp exclude_blocked(query, user, opts) do + blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) + query - |> where([n, a], a.actor not in ^user.blocks) + |> where([n, a], a.actor not in ^blocked_ap_ids) |> where( [n, a], fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks ) end - defp exclude_muted(query, _, %{with_muted: true}) do + defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do query end - defp exclude_muted(query, user, _opts) do + defp exclude_notification_muted(query, user, opts) do + notification_muted_ap_ids = + opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user) + query - |> where([n, a], a.actor not in ^user.muted_notifications) + |> where([n, a], a.actor not in ^notification_muted_ap_ids) |> join(:left, [n, a], tm in Pleroma.ThreadMute, on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) ) |> where([n, a, o, tm], is_nil(tm.user_id)) end + defp exclude_move(query, %{with_move: true}) do + query + end + + defp exclude_move(query, _opts) do + where(query, [n, a], fragment("?->>'type' != 'Move'", a.data)) + end + @valid_visibilities ~w[direct unlisted public private] defp exclude_visibility(query, %{exclude_visibilities: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do query + |> join(:left, [n, a], mutated_activity in Pleroma.Activity, + on: + fragment("?->>'context'", a.data) == + fragment("?->>'context'", mutated_activity.data) and + fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and + fragment("?->>'type'", mutated_activity.data) == "Create", + as: :mutated_activity + ) |> where( - [n, a], + [n, a, mutated_activity: mutated_activity], not fragment( - "activity_visibility(?, ?, ?) = ANY (?)", + """ + CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce' + THEN (activity_visibility(?, ?, ?) = ANY (?)) + ELSE (activity_visibility(?, ?, ?) = ANY (?)) END + """, + a.data, + a.data, + mutated_activity.actor, + mutated_activity.recipients, + mutated_activity.data, + ^visibility, a.actor, a.recipients, a.data, @@ -105,17 +157,7 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility}) defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility in @valid_visibilities do - query - |> where( - [n, a], - not fragment( - "activity_visibility(?, ?, ?) = (?)", - a.actor, - a.recipients, - a.data, - ^visibility - ) - ) + exclude_visibility(query, [visibility]) end defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -251,10 +293,13 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act end end - def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) - when type in ["Like", "Announce", "Follow"] do - users = get_notified_from_activity(activity) - notifications = Enum.map(users, fn user -> create_notification(activity, user) end) + def create_notifications(%Activity{data: %{"type" => type}} = activity) + when type in ["Like", "Announce", "Follow", "Move"] do + notifications = + activity + |> get_notified_from_activity() + |> Enum.map(&create_notification(activity, &1)) + {:ok, notifications} end @@ -276,19 +321,15 @@ def create_notification(%Activity{} = activity, %User{} = user) do def get_notified_from_activity(activity, local_only \\ true) - def get_notified_from_activity( - %Activity{data: %{"to" => _, "type" => type} = _data} = activity, - local_only - ) - when type in ["Create", "Like", "Announce", "Follow"] do - recipients = - [] - |> Utils.maybe_notify_to_recipients(activity) - |> Utils.maybe_notify_mentioned_recipients(activity) - |> Utils.maybe_notify_subscribers(activity) - |> Enum.uniq() - - User.get_users_from_set(recipients, local_only) + def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) + when type in ["Create", "Like", "Announce", "Follow", "Move"] do + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) + |> Utils.maybe_notify_subscribers(activity) + |> Utils.maybe_notify_followers(activity) + |> Enum.uniq() + |> User.get_users_from_set(local_only) end def get_notified_from_activity(_, _local_only), do: [] @@ -314,7 +355,7 @@ def skip?(:self, activity, user) do def skip?( :followers, activity, - %{notification_settings: %{"followers" => false}} = user + %{notification_settings: %{followers: false}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) @@ -324,14 +365,14 @@ def skip?( def skip?( :non_followers, activity, - %{notification_settings: %{"non_followers" => false}} = user + %{notification_settings: %{non_followers: false}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) !User.following?(follower, user) end - def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do + def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) User.following?(user, followed) @@ -340,7 +381,7 @@ def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = def skip?( :non_follows, activity, - %{notification_settings: %{"non_follows" => false}} = user + %{notification_settings: %{non_follows: false}} = user ) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index b4ed3a9b2..ff0e59241 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -147,7 +147,7 @@ def swap_object_with_tombstone(object) do def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), - deleted_activity = Activity.delete_by_ap_id(id), + deleted_activity = Activity.delete_all_by_object_ap_id(id), {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do {:ok, object, deleted_activity} diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index a3278dbef..3201fb399 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do import Plug.Conn import Pleroma.Web.Gettext + alias Pleroma.Config alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @behaviour Plug @@ -15,6 +16,14 @@ def init(%{scopes: _} = options), do: options def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do op = options[:op] || :| token = assigns[:token] + + scopes = + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + matched_scopes = token && filter_descendants(scopes, token.scopes) cond do diff --git a/lib/pleroma/plugs/parsers_plug.ex b/lib/pleroma/plugs/parsers_plug.ex new file mode 100644 index 000000000..2e493ce0e --- /dev/null +++ b/lib/pleroma/plugs/parsers_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Parsers do + @moduledoc "Initializes Plug.Parsers with upload limit set at boot time" + + @behaviour Plug + + def init(_opts) do + Plug.Parsers.init( + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Jason, + length: Pleroma.Config.get([:instance, :upload_limit]), + body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} + ) + end + + defdelegate call(conn, opts), to: Plug.Parsers +end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index ee808f31f..582fb1f92 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -5,19 +5,38 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn + alias Pleroma.User + alias Pleroma.Web.OAuth def init(options) do options end - def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn + def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do + token = assigns[:token] + + cond do + not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> + conn + + token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> + # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. + # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. + conn + + true -> + fail(conn) + end end def call(conn, _) do + fail(conn) + end + + defp fail(conn) do conn - |> render_error(:forbidden, "User is not admin.") - |> halt + |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> halt() end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 601aa9cf0..cebadc8ab 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -7,6 +7,7 @@ defmodule Pleroma.User do import Ecto.Changeset import Ecto.Query + import Ecto, only: [assoc: 2] alias Comeonin.Pbkdf2 alias Ecto.Multi @@ -21,6 +22,7 @@ defmodule Pleroma.User do alias Pleroma.Repo alias Pleroma.RepoStreamer alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils @@ -42,6 +44,32 @@ defmodule Pleroma.User do @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/ + # AP ID user relationships (blocks, mutes etc.) + # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]] + @user_relationships_config [ + block: [ + blocker_blocks: :blocked_users, + blockee_blocks: :blocker_users + ], + mute: [ + muter_mutes: :muted_users, + mutee_mutes: :muter_users + ], + reblog_mute: [ + reblog_muter_mutes: :reblog_muted_users, + reblog_mutee_mutes: :reblog_muter_users + ], + notification_mute: [ + notification_muter_mutes: :notification_muted_users, + notification_mutee_mutes: :notification_muter_users + ], + # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target + inverse_subscription: [ + subscribee_subscriptions: :subscriber_users, + subscriber_subscriptions: :subscribee_users + ] + ] + schema "users" do field(:bio, :string) field(:email, :string) @@ -61,7 +89,6 @@ defmodule Pleroma.User do 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(:source_data, :map, default: %{}) @@ -73,12 +100,7 @@ defmodule Pleroma.User do field(:password_reset_pending, :boolean, default: false) field(:confirmation_token, :string, default: nil) field(:default_scope, :string, default: "public") - field(:blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: []) - field(:mutes, {:array, :string}, default: []) - field(:muted_reblogs, {:array, :string}, default: []) - field(:muted_notifications, {:array, :string}, default: []) - field(:subscribers, {:array, :string}, default: []) field(:deactivated, :boolean, default: false) field(:no_rich_text, :boolean, default: false) field(:ap_enabled, :boolean, default: false) @@ -103,26 +125,97 @@ defmodule Pleroma.User do field(:raw_fields, {:array, :map}, default: []) field(:discoverable, :boolean, default: false) field(:invisible, :boolean, default: false) + field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) + field(:actor_type, :string, default: "Person") + field(:also_known_as, {:array, :string}, default: []) - field(:notification_settings, :map, - default: %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } + embeds_one( + :notification_settings, + Pleroma.User.NotificationSetting, + on_replace: :update ) has_many(:notifications, Notification) has_many(:registrations, Registration) has_many(:deliveries, Delivery) - field(:info, :map, default: %{}) + has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id) + has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id) + + for {relationship_type, + [ + {outgoing_relation, outgoing_relation_target}, + {incoming_relation, incoming_relation_source} + ]} <- @user_relationships_config do + # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc. + has_many(outgoing_relation, UserRelationship, + foreign_key: :source_id, + where: [relationship_type: relationship_type] + ) + + # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc. + has_many(incoming_relation, UserRelationship, + foreign_key: :target_id, + where: [relationship_type: relationship_type] + ) + + # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc. + has_many(outgoing_relation_target, through: [outgoing_relation, :target]) + + # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc. + has_many(incoming_relation_source, through: [incoming_relation, :source]) + end + + # `:blocks` is deprecated (replaced with `blocked_users` relation) + field(:blocks, {:array, :string}, default: []) + # `:mutes` is deprecated (replaced with `muted_users` relation) + field(:mutes, {:array, :string}, default: []) + # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation) + field(:muted_reblogs, {:array, :string}, default: []) + # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation) + field(:muted_notifications, {:array, :string}, default: []) + # `:subscribers` is deprecated (replaced with `subscriber_users` relation) + field(:subscribers, {:array, :string}, default: []) timestamps() end + for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- + @user_relationships_config do + # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc. + def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do + target_users_query = assoc(user, unquote(outgoing_relation_target)) + + if restrict_deactivated? do + restrict_deactivated(target_users_query) + else + target_users_query + end + end + + # Definitions of `blocked_users/1`, `muted_users/1`, etc. + def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do + __MODULE__ + |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ + user, + restrict_deactivated? + ]) + |> Repo.all() + end + + # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc. + def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do + __MODULE__ + |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ + user, + restrict_deactivated? + ]) + |> select([u], u.ap_id) + |> Repo.all() + end + end + @doc "Returns if the user should be allowed to authenticate" def auth_active?(%User{deactivated: true}), do: false @@ -225,7 +318,6 @@ def remote_user_creation(params) do params = params - |> Map.put(:info, params[:info] || %{}) |> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:bio, bio_limit) |> truncate_fields_param() @@ -254,7 +346,9 @@ def remote_user_creation(params) do :fields, :following_count, :discoverable, - :invisible + :invisible, + :actor_type, + :also_known_as ] ) |> validate_required([:name, :ap_id]) @@ -296,13 +390,16 @@ def update_changeset(struct, params \\ %{}) do :hide_followers_count, :hide_follows_count, :hide_favorites, + :allow_following_move, :background, :show_role, :skip_thread_containment, :fields, :raw_fields, :pleroma_settings_store, - :discoverable + :discoverable, + :actor_type, + :also_known_as ] ) |> unique_constraint(:nickname) @@ -340,9 +437,12 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do :hide_follows, :fields, :hide_followers, + :allow_following_move, :discoverable, :hide_followers_count, - :hide_follows_count + :hide_follows_count, + :actor_type, + :also_known_as ] ) |> unique_constraint(:nickname) @@ -949,34 +1049,45 @@ def get_recipients_from_activity(%Activity{recipients: to}) do |> Repo.all() end - @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} - def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do - add_to_mutes(muter, ap_id, notifications?) + @spec mute(User.t(), User.t(), boolean()) :: + {:ok, list(UserRelationship.t())} | {:error, String.t()} + def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do + add_to_mutes(muter, mutee, notifications?) end - def unmute(muter, %{ap_id: ap_id}) do - remove_from_mutes(muter, ap_id) + def unmute(%User{} = muter, %User{} = mutee) do + remove_from_mutes(muter, mutee) end - def subscribe(subscriber, %{ap_id: ap_id}) do - with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do - deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + def subscribe(%User{} = subscriber, %User{} = target) do + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) - if blocks?(subscribed, subscriber) and deny_follow_blocked do - {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} - else - User.add_to_subscribers(subscribed, subscriber.ap_id) - end + 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 unsubscribe(unsubscriber, %{ap_id: ap_id}) do + 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 - User.remove_from_subscribers(user, unsubscriber.ap_id) + unsubscribe(unsubscriber, user) end end - def block(blocker, %User{ap_id: ap_id} = blocked) do + 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 @@ -993,51 +1104,54 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do nil -> blocked end - blocker = - if subscribed_to?(blocked, blocker) do - {:ok, blocker} = unsubscribe(blocked, blocker) - blocker - else - blocker - end + unsubscribe(blocked, blocker) if following?(blocked, blocker), do: unfollow(blocked, blocker) {:ok, blocker} = update_follower_count(blocker) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) - add_to_block(blocker, ap_id) + add_to_block(blocker, blocked) end # helper to handle the block given only an actor's AP id - def block(blocker, %{ap_id: ap_id}) do + def block(%User{} = blocker, %{ap_id: ap_id}) do block(blocker, get_cached_by_ap_id(ap_id)) end - def unblock(blocker, %{ap_id: ap_id}) do - remove_from_block(blocker, ap_id) + 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, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id) + 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, %{ap_id: ap_id}), - do: Enum.member?(user.muted_notifications, ap_id) + 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_ap_id?(user, target) || (!User.following?(user, target) && blocks_domain?(user, target)) end - def blocks?(nil, _), do: false - - def blocks_ap_id?(%User{} = user, %User{} = target) do - Enum.member?(user.blocks, target.ap_id) + def blocks_user?(%User{} = user, %User{} = target) do + UserRelationship.block_exists?(user, target) end - def blocks_ap_id?(_, _), do: false + def blocks_user?(_, _), do: false def blocks_domain?(%User{} = user, %User{} = target) do domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) @@ -1047,28 +1161,41 @@ def blocks_domain?(%User{} = user, %User{} = target) do def blocks_domain?(_, _), do: false - def subscribed_to?(user, %{ap_id: ap_id}) do + 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 - Enum.member?(target.subscribers, user.ap_id) + subscribed_to?(user, target) end end - @spec muted_users(User.t()) :: [User.t()] - def muted_users(user) do - User.Query.build(%{ap_id: user.mutes, deactivated: false}) - |> Repo.all() - end + @doc """ + Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. + E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` + """ + @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} + def outgoing_relations_ap_ids(_, []), do: %{} - @spec blocked_users(User.t()) :: [User.t()] - def blocked_users(user) do - User.Query.build(%{ap_id: user.blocks, deactivated: false}) - |> Repo.all() - end + def outgoing_relations_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) - @spec subscribers(User.t()) :: [User.t()] - def subscribers(user) do - User.Query.build(%{ap_id: user.subscribers, deactivated: false}) - |> Repo.all() + Enum.into( + relationship_types, + %{}, + fn rel_type -> {rel_type, db_result[rel_type] || []} end + ) end def deactivate_async(user, status \\ true) do @@ -1103,20 +1230,9 @@ def deactivate(%User{} = user, status) do end def update_notification_settings(%User{} = user, settings) do - settings = - settings - |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) - |> Map.new() - - notification_settings = - user.notification_settings - |> Map.merge(settings) - |> Map.take(["followers", "follows", "non_follows", "non_followers"]) - - params = %{notification_settings: notification_settings} - user - |> cast(params, [:notification_settings]) + |> cast(%{notification_settings: settings}, []) + |> cast_embed(:notification_settings) |> validate_required([:notification_settings]) |> update_and_set_cache() end @@ -1175,7 +1291,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers) blocked_identifiers, fn blocked_identifier -> with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, blocker} <- block(blocker, blocked), + {:ok, _user_block} <- block(blocker, blocked), {:ok, _} <- ActivityPub.block(blocker, blocked) do blocked else @@ -1219,7 +1335,7 @@ def external_users_query do def external_users(opts \\ []) do query = external_users_query() - |> select([u], struct(u, [:id, :ap_id, :info])) + |> select([u], struct(u, [:id, :ap_id])) query = if opts[:max_id], @@ -1489,7 +1605,7 @@ def all_superusers do end def showing_reblogs?(%User{} = user, %User{} = target) do - target.ap_id not in user.muted_reblogs + not UserRelationship.reblog_mute_exists?(user, target) end @doc """ @@ -1731,13 +1847,28 @@ defp truncate_field(%{"name" => name, "value" => value}) do end def admin_api_update(user, params) do - user - |> cast(params, [ - :is_moderator, - :is_admin, - :show_role - ]) - |> update_and_set_cache() + changeset = + cast(user, params, [ + :is_moderator, + :is_admin, + :show_role + ]) + + with {:ok, updated_user} <- update_and_set_cache(changeset) do + if user.is_admin && !updated_user.is_admin do + # Tokens & authorizations containing any admin scopes must be revoked (revoking all). + # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). + global_sign_out(user) + end + + {:ok, updated_user} + end + 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 @@ -1812,23 +1943,6 @@ def update_email_notifications(user, settings) do |> update_and_set_cache() end - defp set_subscribers(user, subscribers) do - params = %{subscribers: subscribers} - - user - |> cast(params, [:subscribers]) - |> validate_required([:subscribers]) - |> update_and_set_cache() - end - - def add_to_subscribers(user, subscribed) do - set_subscribers(user, Enum.uniq([subscribed | user.subscribers])) - end - - def remove_from_subscribers(user, subscribed) do - set_subscribers(user, List.delete(user.subscribers, subscribed)) - end - defp set_domain_blocks(user, domain_blocks) do params = %{domain_blocks: domain_blocks} @@ -1846,81 +1960,35 @@ def unblock_domain(user, domain_blocked) do set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) end - defp set_blocks(user, blocks) do - params = %{blocks: blocks} - - user - |> cast(params, [:blocks]) - |> validate_required([:blocks]) - |> update_and_set_cache() + @spec add_to_block(User.t(), User.t()) :: + {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} + defp add_to_block(%User{} = user, %User{} = blocked) do + UserRelationship.create_block(user, blocked) end - def add_to_block(user, blocked) do - set_blocks(user, Enum.uniq([blocked | user.blocks])) + @spec add_to_block(User.t(), User.t()) :: + {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} + defp remove_from_block(%User{} = user, %User{} = blocked) do + UserRelationship.delete_block(user, blocked) end - def remove_from_block(user, blocked) do - set_blocks(user, List.delete(user.blocks, blocked)) - end - - defp set_mutes(user, mutes) do - params = %{mutes: mutes} - - user - |> cast(params, [:mutes]) - |> validate_required([:mutes]) - |> update_and_set_cache() - end - - def add_to_mutes(user, muted, notifications?) do - with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do - set_notification_mutes( - user, - Enum.uniq([muted | user.muted_notifications]), - notifications? - ) + defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do + with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user), + {:ok, user_notification_mute} <- + (notifications? && UserRelationship.create_notification_mute(user, muted_user)) || + {:ok, nil} do + {:ok, Enum.filter([user_mute, user_notification_mute], & &1)} end end - def remove_from_mutes(user, muted) do - with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do - set_notification_mutes( - user, - List.delete(user.muted_notifications, muted), - true - ) + defp remove_from_mutes(user, %User{} = muted_user) do + with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user), + {:ok, user_notification_mute} <- + UserRelationship.delete_notification_mute(user, muted_user) do + {:ok, [user_mute, user_notification_mute]} end end - defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do - {:ok, user} - end - - defp set_notification_mutes(user, muted_notifications, true = _notifications?) do - params = %{muted_notifications: muted_notifications} - - user - |> cast(params, [:muted_notifications]) - |> validate_required([:muted_notifications]) - |> update_and_set_cache() - end - - def add_reblog_mute(user, ap_id) do - params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]} - - user - |> cast(params, [:muted_reblogs]) - |> update_and_set_cache() - end - - def remove_reblog_mute(user, ap_id) do - params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)} - - user - |> cast(params, [:muted_reblogs]) - |> update_and_set_cache() - end - def set_invisible(user, invisible) do params = %{invisible: invisible} diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex new file mode 100644 index 000000000..f0899613e --- /dev/null +++ b/lib/pleroma/user/notification_setting.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSetting do + use Ecto.Schema + import Ecto.Changeset + + @derive Jason.Encoder + @primary_key false + + embedded_schema do + field(:followers, :boolean, default: true) + field(:follows, :boolean, default: true) + field(:non_follows, :boolean, default: true) + field(:non_followers, :boolean, default: true) + field(:privacy_option, :boolean, default: false) + end + + def changeset(schema, params) do + schema + |> cast(prepare_attrs(params), [ + :followers, + :follows, + :non_follows, + :non_followers, + :privacy_option + ]) + end + + defp prepare_attrs(params) do + Enum.reduce(params, %{}, fn + {k, v}, acc when is_binary(v) -> + Map.put(acc, k, String.downcase(v)) + + {k, v}, acc -> + Map.put(acc, k, v) + end) + end +end diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index b1bb9d4da..6b55df483 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -103,9 +103,13 @@ defp filter_invisible_users(query) do from(q in query, where: q.invisible == false) end - defp filter_blocked_user(query, %User{blocks: blocks}) - when length(blocks) > 0 do - from(q in query, where: not (q.ap_id in ^blocks)) + defp filter_blocked_user(query, %User{} = blocker) do + query + |> join(:left, [u], b in Pleroma.UserRelationship, + as: :blocks, + on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id + ) + |> where([blocks: b], is_nil(b.target_id)) end defp filter_blocked_user(query, _), do: query diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex new file mode 100644 index 000000000..24c724549 --- /dev/null +++ b/lib/pleroma/user_relationship.ex @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationship do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserRelationship + + schema "user_relationships" do + belongs_to(:source, User, type: FlakeId.Ecto.CompatType) + belongs_to(:target, User, type: FlakeId.Ecto.CompatType) + field(:relationship_type, UserRelationshipTypeEnum) + + timestamps(updated_at: false) + end + + for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do + # Definitions of `create_block/2`, `create_mute/2` etc. + def unquote(:"create_#{relationship_type}")(source, target), + do: create(unquote(relationship_type), source, target) + + # Definitions of `delete_block/2`, `delete_mute/2` etc. + def unquote(:"delete_#{relationship_type}")(source, target), + do: delete(unquote(relationship_type), source, target) + + # Definitions of `block_exists?/2`, `mute_exists?/2` etc. + def unquote(:"#{relationship_type}_exists?")(source, target), + do: exists?(unquote(relationship_type), source, target) + end + + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do + user_relationship + |> cast(params, [:relationship_type, :source_id, :target_id]) + |> validate_required([:relationship_type, :source_id, :target_id]) + |> unique_constraint(:relationship_type, + name: :user_relationships_source_id_relationship_type_target_id_index + ) + |> validate_not_self_relationship() + end + + def exists?(relationship_type, %User{} = source, %User{} = target) do + UserRelationship + |> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id) + |> Repo.exists?() + end + + def create(relationship_type, %User{} = source, %User{} = target) do + %UserRelationship{} + |> changeset(%{ + relationship_type: relationship_type, + source_id: source.id, + target_id: target.id + }) + |> Repo.insert( + on_conflict: :replace_all_except_primary_key, + conflict_target: [:source_id, :relationship_type, :target_id] + ) + end + + def delete(relationship_type, %User{} = source, %User{} = target) do + attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id} + + case Repo.get_by(UserRelationship, attrs) do + %UserRelationship{} = existing_record -> Repo.delete(existing_record) + nil -> {:ok, nil} + end + end + + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do + changeset + |> validate_change(:target_id, fn _, target_id -> + if target_id == get_field(changeset, :source_id) do + [target_id: "can't be equal to source_id"] + else + [] + end + end) + |> validate_change(:source_id, fn _, source_id -> + if source_id == get_field(changeset, :target_id) do + [source_id: "can't be equal to target_id"] + else + [] + end + end) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 29cc1a17b..c6744e5f2 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -456,17 +456,18 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ [ user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) - with {:ok, object, activity} <- Object.delete(object), + with create_activity <- Activity.get_create_by_object_ap_id(id), data <- %{ "type" => "Delete", "actor" => actor, "object" => id, "to" => to, - "deleted_activity_id" => activity && activity.id + "deleted_activity_id" => create_activity && create_activity.id } |> maybe_put("id", activity_id), {:ok, activity} <- insert(data, local, false), + {:ok, object, _create_activity} <- Object.delete(object), stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), {:ok, _actor} <- decrease_note_count_if_public(user, object), @@ -541,6 +542,30 @@ def flag( end end + def move(%User{} = origin, %User{} = target, local \\ true) do + params = %{ + "type" => "Move", + "actor" => origin.ap_id, + "object" => origin.ap_id, + "target" => target.ap_id + } + + with true <- origin.ap_id in target.also_known_as, + {:ok, activity} <- insert(params, local) do + maybe_federate(activity) + + BackgroundWorker.enqueue("move_following", %{ + "origin_id" => origin.id, + "target_id" => target.id + }) + + {:ok, activity} + else + false -> {:error, "Target account must have the origin in `alsoKnownAs`"} + err -> err + end + end + defp fetch_activities_for_context_query(context, opts) do public = [Pleroma.Constants.as_public()] @@ -724,6 +749,15 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do |> Map.put("whole_db", true) |> Map.put("pinned_activity_ids", user.pinned_activities) + params = + if User.blocks?(reading_user, user) do + params + else + params + |> Map.put("blocking_user", reading_user) + |> Map.put("muting_user", reading_user) + end + recipients = user_activities_recipients(%{ "godmode" => params["godmode"], @@ -895,7 +929,7 @@ defp restrict_reblogs(query, _), do: query defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = user.mutes + mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -912,8 +946,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do - blocks = user.blocks || [] + 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 = @@ -925,14 +959,14 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do from( [activity, object: o] in query, - where: fragment("not (? = ANY(?))", activity.actor, ^blocks), - where: fragment("not (? && ?)", activity.recipients, ^blocks), + where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), + where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), where: fragment( "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", activity.data, activity.data, - ^blocks + ^blocked_ap_ids ), where: fragment( @@ -973,8 +1007,8 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) defp restrict_pinned(query, _), do: query - defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do - muted_reblogs = user.muted_reblogs || [] + 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, @@ -1055,7 +1089,33 @@ defp maybe_order(query, %{order: :asc}) do defp maybe_order(query, _), do: query + defp fetch_activities_query_ap_ids_ops(opts) do + source_user = opts["muting_user"] + ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] + + ap_id_relations = + ap_id_relations ++ + if opts["blocking_user"] && opts["blocking_user"] == source_user do + [:block] + else + [] + end + + preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) + + restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) + restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) + + restrict_muted_reblogs_opts = + Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + + {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} + end + def fetch_activities_query(recipients, opts \\ %{}) do + {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} = + fetch_activities_query_ap_ids_ops(opts) + config = %{ skip_thread_containment: Config.get([:instance, :skip_thread_containment]) } @@ -1075,15 +1135,15 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_type(opts) |> restrict_state(opts) |> restrict_favorited_by(opts) - |> restrict_blocked(opts) - |> restrict_muted(opts) + |> restrict_blocked(restrict_blocked_opts) + |> restrict_muted(restrict_muted_opts) |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) - |> restrict_muted_reblogs(opts) + |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) @@ -1175,6 +1235,7 @@ defp object_to_user_data(data) do data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false + actor_type = data["type"] || "Person" user_data = %{ ap_id: data["id"], @@ -1189,7 +1250,9 @@ defp object_to_user_data(data) do name: data["name"], follower_address: data["followers"], following_address: data["following"], - bio: data["summary"] + bio: data["summary"], + actor_type: actor_type, + also_known_as: Map.get(data, "alsoKnownAs", []) } # nickname can be nil because of virtual actors @@ -1251,13 +1314,13 @@ defp maybe_update_follow_information(data) do end end - defp collection_private(data) do - if is_map(data["first"]) and - data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do + defp collection_private(%{"first" => first}) do + if is_map(first) and + first["type"] in ["CollectionPage", "OrderedCollectionPage"] do {:ok, false} else with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- - Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do + Fetcher.fetch_and_contain_remote_object_from_id(first) do {:ok, false} else {:error, {:ok, %{status: code}}} when code in [401, 403] -> @@ -1272,6 +1335,8 @@ defp collection_private(data) do end end + defp collection_private(_data), do: {:ok, true} + def user_data_from_user_object(data) do with {:ok, data} <- MRF.filter(data), {:ok, data} <- object_to_user_data(data) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 15612545b..ecba27bef 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -387,7 +387,7 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = def handle_incoming(%{"id" => nil}, _options), do: :error def handle_incoming(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), + def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, do: :error # TODO: validate those with a Ecto scheme @@ -669,7 +669,7 @@ def handle_incoming( update_data = new_user_data - |> Map.take([:avatar, :banner, :bio, :name]) + |> Map.take([:avatar, :banner, :bio, :name, :also_known_as]) |> Map.put(:fields, fields) |> Map.put(:locked, locked) |> Map.put(:invisible, invisible) @@ -857,6 +857,24 @@ def handle_incoming( end end + def handle_incoming( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do + with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), + {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), + true <- origin_actor in target_user.also_known_as do + ActivityPub.move(origin_user, target_user, false) + else + _e -> :error + end + end + def handle_incoming(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 01aacbde3..2ca805c09 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -722,16 +722,22 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do act when is_binary(act) -> act end - activity = Activity.get_by_ap_id_with_object(id) - actor = User.get_by_ap_id(activity.object.data["actor"]) + case Activity.get_by_ap_id_with_object(id) do + %Activity{} = activity -> + %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => activity.object.data["content"], + "published" => activity.object.data["published"], + "actor" => + AccountView.render("show.json", %{ + user: User.get_by_ap_id(activity.object.data["actor"]) + }) + } - %{ - "type" => "Note", - "id" => activity.data["id"], - "content" => activity.object.data["content"], - "published" => activity.object.data["published"], - "actor" => AccountView.render("show.json", %{user: actor}) - } + _ -> + %{"id" => id, "deleted" => true} + end end defp build_flag_object(_), do: [] @@ -788,7 +794,52 @@ def get_reports(params, page, page_size) do ActivityPub.fetch_activities([], params, :offset) end - @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ + def parse_report_group(activity) do + reports = get_reports_by_status_id(activity["id"]) + max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) + actors = Enum.map(reports, & &1.user_actor) + [%{data: %{"object" => [account_id | _]}} | _] = reports + + account = + AccountView.render("show.json", %{ + user: User.get_by_ap_id(account_id) + }) + + status = get_status_data(activity) + + %{ + date: max_date.data["published"], + account: account, + status: status, + actors: Enum.uniq(actors), + reports: reports + } + end + + defp get_status_data(status) do + case status["deleted"] do + true -> + %{ + "id" => status["id"], + "deleted" => true + } + + _ -> + Activity.get_by_ap_id(status["id"]) + end + end + + def get_reports_by_status_id(ap_id) do + from(a in Activity, + where: fragment("(?)->>'type' = 'Flag'", a.data), + where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]), + or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id]) + ) + |> Activity.with_preloaded_user_actor() + |> Repo.all() + end + + @spec get_reports_grouped_by_status([String.t()]) :: %{ required(:groups) => [ %{ required(:date) => String.t(), @@ -797,20 +848,15 @@ def get_reports(params, page, page_size) do required(:actors) => [%User{}], required(:reports) => [%Activity{}] } - ], - required(:total) => integer + ] } - def get_reports_grouped_by_status(groups) do + def get_reports_grouped_by_status(activity_ids) do parsed_groups = - groups - |> Enum.map(fn entry -> - activity = - case Jason.decode(entry.activity) do - {:ok, activity} -> activity - _ -> build_flag_object(entry.activity) - end - - parse_report_group(activity) + activity_ids + |> Enum.map(fn id -> + id + |> build_flag_object() + |> parse_report_group() end) %{ @@ -818,33 +864,6 @@ def get_reports_grouped_by_status(groups) do } end - def parse_report_group(activity) do - reports = get_reports_by_status_id(activity["id"]) - max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) - actors = Enum.map(reports, & &1.user_actor) - - %{ - date: max_date.data["published"], - account: activity["actor"], - status: %{ - id: activity["id"], - content: activity["content"], - published: activity["published"] - }, - actors: Enum.uniq(actors), - reports: reports - } - end - - def get_reports_by_status_id(ap_id) do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), - where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]) - ) - |> Activity.with_preloaded_user_actor() - |> Repo.all() - end - @spec get_reported_activities() :: [ %{ required(:activity) => String.t(), @@ -852,17 +871,23 @@ def get_reports_by_status_id(ap_id) do } ] def get_reported_activities do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), + reported_activities_query = + from(a in Activity, + where: fragment("(?)->>'type' = 'Flag'", a.data), + select: %{ + activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data) + }, + group_by: fragment("activity") + ) + + from(a in subquery(reported_activities_query), + distinct: true, select: %{ - date: fragment("max(?->>'published') date", a.data), - activity: - fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data) - }, - group_by: fragment("activity"), - order_by: fragment("date DESC") + id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity) + } ) |> Repo.all() + |> Enum.map(& &1.id) end def update_report_state(%Activity{} = activity, state) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index cf08045c9..9059aa634 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -91,7 +91,7 @@ def render("user.json", %{user: user}) do %{ "id" => user.ap_id, - "type" => "Person", + "type" => user.actor_type, "following" => "#{user.ap_id}/following", "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index cd4097493..e172f6d3f 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do @spec is_public?(Object.t() | Activity.t() | map()) :: boolean() def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: data}), do: is_public?(data) + def is_public?(%Activity{data: %{"type" => "Move"}}), do: true def is_public?(%Activity{data: data}), do: is_public?(data) def is_public?(%{"directMessage" => true}), do: false def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 24fdc3c82..0a8a56cd8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:accounts"]} + %{scopes: ["read:accounts"], admin: true} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["write:accounts"]} + %{scopes: ["write:accounts"], admin: true} when action in [ :get_invite_token, :revoke_invite, @@ -58,35 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:reports"]} when action in [:list_reports, :report_show] + %{scopes: ["read:reports"], admin: true} + when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: ["write:reports"]} + %{scopes: ["write:reports"], admin: true} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action == :list_user_statuses + %{scopes: ["read:statuses"], admin: true} + when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} + %{scopes: ["write:statuses"], admin: true} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: ["read"]} + %{scopes: ["read"], admin: true} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["write"], admin: true} when action in [:relay_follow, :relay_unfollow, :config_update] ) @@ -647,11 +649,11 @@ def list_reports(conn, params) do end def list_grouped_reports(conn, _params) do - reports = Utils.get_reported_activities() + statuses = Utils.get_reported_activities() conn |> put_view(ReportView) - |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports)) + |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses)) end def report_show(conn, %{"id" => id}) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca88595c7..13602efd9 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do use Pleroma.Web, :view + alias Pleroma.Activity alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.AdminAPI.Report @@ -45,10 +46,16 @@ def render("show.json", %{report: report, user: user, account: account, statuses def render("index_grouped.json", %{groups: groups}) do reports = Enum.map(groups, fn group -> + status = + case group.status do + %Activity{} = activity -> StatusView.render("show.json", %{activity: activity}) + _ -> group.status + end + %{ date: group[:date], account: group[:account], - status: group[:status], + status: Map.put_new(status, "deleted", false), actors: Enum.map(group[:actors], &merge_account_views/1), reports: group[:reports] diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index 08841a3e8..840414933 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -20,7 +20,7 @@ def handle_info(:after_join, socket) do def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do text = String.trim(text) - if String.length(text) > 0 do + if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do author = User.get_cached_by_nickname(user_name) author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author) message = ChatChannelState.add_message(%{text: text, author: author}) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fe6e26a90..2f3bcfc3c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -32,7 +33,7 @@ def follow(follower, followed) do def unfollow(follower, unfollowed) do with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), - {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do + {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do {:ok, follower} end end @@ -420,15 +421,11 @@ defp set_visibility(activity, %{"visibility" => visibility}) do defp set_visibility(activity, _), do: {:ok, activity} - def hide_reblogs(user, %{ap_id: ap_id} = _muted) do - if ap_id not in user.muted_reblogs do - User.add_reblog_mute(user, ap_id) - end + def hide_reblogs(%User{} = user, %User{} = target) do + UserRelationship.create_reblog_mute(user, target) end - def show_reblogs(user, %{ap_id: ap_id} = _muted) do - if ap_id in user.muted_reblogs do - User.remove_reblog_mute(user, ap_id) - end + def show_reblogs(%User{} = user, %User{} = target) do + UserRelationship.delete_reblog_mute(user, target) end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 88a5f434a..a9b164d9a 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -451,6 +451,8 @@ def maybe_notify_to_recipients( recipients ++ to end + def maybe_notify_to_recipients(recipients, _), do: recipients + def maybe_notify_mentioned_recipients( recipients, %Activity{data: %{"to" => _to, "type" => type} = data} = activity @@ -492,7 +494,7 @@ def maybe_notify_subscribers( with %User{} = user <- User.get_cached_by_ap_id(actor) do subscriber_ids = user - |> User.subscribers() + |> User.subscriber_users() |> Enum.filter(&Visibility.visible_for_user?(activity, &1)) |> Enum.map(& &1.ap_id) @@ -502,6 +504,17 @@ def maybe_notify_subscribers( def maybe_notify_subscribers(recipients, _), do: recipients + def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do + with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do + user + |> User.get_followers() + |> Enum.map(& &1.ap_id) + |> Enum.concat(recipients) + end + end + + def maybe_notify_followers(recipients, _), do: recipients + def maybe_extract_mentions(%{"tag" => tag}) do tag |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 49735b5c2..bbea31682 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -61,14 +61,7 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.RequestId) plug(Plug.Logger) - plug( - Plug.Parsers, - parsers: [:urlencoded, :multipart, :json], - pass: ["*/*"], - json_decoder: Jason, - length: Pleroma.Config.get([:instance, :upload_limit]), - body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} - ) + plug(Pleroma.Plugs.Parsers) plug(Plug.MethodOverride) plug(Plug.Head) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 5f2544640..38d14256f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -152,6 +152,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do :hide_favorites, :show_role, :skip_thread_containment, + :allow_following_move, :discoverable ] |> Enum.reduce(%{}, fn key, acc -> @@ -187,6 +188,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do {:ok, Map.merge(user.pleroma_settings_store, value)} end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "actor_type", :actor_type) emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") @@ -248,7 +250,11 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do @doc "GET /api/v1/accounts/:id/statuses" def statuses(%{assigns: %{user: reading_user}} = conn, params) do with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do - params = Map.put(params, "tag", params["tagged"]) + params = + params + |> Map.put("tag", params["tagged"]) + |> Map.delete("godmode") + activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -323,7 +329,7 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do notifications? = params |> Map.get("notifications", true) |> truthy_param?() - with {:ok, muter} <- User.mute(muter, muted, notifications?) do + with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -332,7 +338,7 @@ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do @doc "POST /api/v1/accounts/:id/unmute" def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do - with {:ok, muter} <- User.unmute(muter, muted) do + with {:ok, _user_relationships} <- User.unmute(muter, muted) do render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -341,7 +347,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do @doc "POST /api/v1/accounts/:id/block" def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, blocker} <- User.block(blocker, blocked), + with {:ok, _user_block} <- User.block(blocker, blocked), {:ok, _activity} <- ActivityPub.block(blocker, blocked) do render(conn, "relationship.json", user: blocker, target: blocked) else @@ -351,7 +357,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do @doc "POST /api/v1/accounts/:id/unblock" def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, blocker} <- User.unblock(blocker, blocked), + with {:ok, _user_block} <- User.unblock(blocker, blocked), {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do render(conn, "relationship.json", user: blocker, target: blocked) else @@ -373,12 +379,14 @@ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do @doc "GET /api/v1/mutes" def mutes(%{assigns: %{user: user}} = conn, _) do - render(conn, "index.json", users: User.muted_users(user), for: user, as: :user) + users = User.muted_users(user, _restrict_deactivated = true) + render(conn, "index.json", users: users, for: user, as: :user) end @doc "GET /api/v1/blocks" def blocks(%{assigns: %{user: user}} = conn, _) do - render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user) + users = User.blocked_users(user, _restrict_deactivated = true) + render(conn, "index.json", users: users, for: user, as: :user) end @doc "GET /api/v1/endorsements" diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index d875a5788..b1816370e 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -24,19 +24,16 @@ def follow(follower, followed, params \\ %{}) do with {:ok, follower, _followed, _} <- result do options = cast_params(params) - - case reblogs_visibility(options[:reblogs], result) do - {:ok, follower} -> {:ok, follower} - _ -> {:ok, follower} - end + set_reblogs_visibility(options[:reblogs], result) + {:ok, follower} end end - defp reblogs_visibility(false, {:ok, follower, followed, _}) do + defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do CommonAPI.hide_reblogs(follower, followed) end - defp reblogs_visibility(_, {:ok, follower, followed, _}) do + defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do CommonAPI.show_reblogs(follower, followed) end @@ -73,7 +70,8 @@ defp cast_params(params) do exclude_types: {:array, :string}, exclude_visibilities: {:array, :string}, reblogs: :boolean, - with_muted: :boolean + with_muted: :boolean, + with_move: :boolean } changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 1068f8823..a5420f480 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -50,8 +50,8 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target id: to_string(target.id), following: User.following?(user, target), followed_by: User.following?(target, user), - blocking: User.blocks_ap_id?(user, target), - blocked_by: User.blocks_ap_id?(target, user), + blocking: User.blocks_user?(user, target), + blocked_by: User.blocks_user?(target, user), muting: User.mutes?(user, target), muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), @@ -86,7 +86,7 @@ defp do_render("show.json", %{user: user} = opts) do 0 end - bot = (user.source_data["type"] || "Person") in ["Application", "Service"] + bot = user.actor_type in ["Application", "Service"] emojis = (user.source_data["tag"] || []) @@ -137,7 +137,8 @@ defp do_render("show.json", %{user: user} = opts) do sensitive: false, fields: user.raw_fields, pleroma: %{ - discoverable: user.discoverable + discoverable: user.discoverable, + actor_type: user.actor_type } }, @@ -162,6 +163,7 @@ defp do_render("show.json", %{user: user} = opts) do |> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_activation_status(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for]) + |> maybe_put_allow_following_move(user, opts[:for]) |> maybe_put_unread_conversation_count(user, opts[:for]) end @@ -238,6 +240,12 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: defp maybe_put_notification_settings(data, _, _), do: data + defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do + Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move) + end + + defp maybe_put_allow_following_move(data, _, _), do: data + defp maybe_put_activation_status(data, user, %User{is_admin: true}) do Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated) end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 5e3dbe728..ddd7f5318 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -37,32 +37,24 @@ def render("show.json", %{ } case mastodon_type do - "mention" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: activity, for: user}) - }) - - "favourite" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: parent_activity, for: user}) - }) - - "reblog" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: parent_activity, for: user}) - }) - - "follow" -> - response - - _ -> - nil + "mention" -> put_status(response, activity, user) + "favourite" -> put_status(response, parent_activity, user) + "reblog" -> put_status(response, parent_activity, user) + "move" -> put_target(response, activity, user) + "follow" -> response + _ -> nil end else _ -> nil end end + + defp put_status(response, activity, user) do + Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) + end + + defp put_target(response, activity, user) do + target = User.get_cached_by_ap_id(activity.data["target"]) + Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 2aee8cab2..87acdec97 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ def token_exchange( {:user_active, true} <- {:user_active, !user.deactivated}, {:password_reset_pending, false} <- {:password_reset_pending, user.password_reset_pending}, - {:ok, scopes} <- validate_scopes(app, params), + {:ok, scopes} <- validate_scopes(app, params, user), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defp do_create_authorization( {: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), - {:ok, scopes} <- validate_scopes(app, auth_attrs), + {:ok, scopes} <- validate_scopes(app, auth_attrs, user), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end @@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) - @spec validate_scopes(App.t(), map()) :: + @spec validate_scopes(App.t(), map(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(app, params) do + defp validate_scopes(%App{} = app, params, %User{} = user) do params |> Scopes.fetch_scopes(app.scopes) - |> Scopes.validate(app.scopes) + |> Scopes.validate(app.scopes, user) end def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 48bd14407..5e04652c2 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do Functions for dealing with scopes. """ + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + @doc """ Fetch scopes from request params. @@ -53,15 +56,36 @@ def to_string(scopes), do: Enum.join(scopes, " ") @doc """ Validates scopes. """ - @spec validate(list() | nil, list()) :: + @spec validate(list() | nil, list(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate([], _app_scopes), do: {:error, :missing_scopes} - def validate(nil, _app_scopes), do: {:error, :missing_scopes} + def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} - def validate(scopes, app_scopes) do - case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + def validate(scopes, app_scopes, %User{} = user) do + with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), + {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do + {:ok, scopes} + end + end + + defp ensure_scopes_support(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do ^scopes -> {:ok, scopes} _ -> {:error, :unsupported_scopes} end end + + defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do + if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do + {:ok, scopes} + else + {:error, :unsupported_scopes} + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end end diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index f639f9c6f..3c9c580d5 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -11,11 +11,6 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do @ten_seconds 10_000 @one_day 86_400_000 - @interval Pleroma.Config.get( - [:oauth2, :clean_expired_tokens_interval], - @one_day - ) - alias Pleroma.Web.OAuth.Token alias Pleroma.Workers.BackgroundWorker @@ -29,8 +24,9 @@ def init(_) do @doc false def handle_info(:perform, state) do BackgroundWorker.enqueue("clean_expired_tokens", %{}) + interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day) - Process.send_after(self(), :perform, @interval) + Process.send_after(self(), :perform, interval) {:noreply, state} end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index bc2f1017c..773cd9a97 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -144,7 +144,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do @doc "POST /api/v1/pleroma/accounts/:id/subscribe" def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do - with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do + with {:ok, _subscription} <- User.subscribe(user, subscription_target) do render(conn, "relationship.json", user: user, target: subscription_target) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -153,7 +153,7 @@ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _p @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe" def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do - with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do + with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do render(conn, "relationship.json", user: user, target: subscription_target) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index a474d41d4..69dfa92e3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["write"], admin: true} when action in [ :create, :delete, diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 3de7af708..34ec1d8d9 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,14 +16,14 @@ defmodule Pleroma.Web.Push.Impl do require Logger import Ecto.Query - @types ["Create", "Follow", "Announce", "Like"] + @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" @spec perform(Notification.t()) :: list(any) | :error def perform( %{ - activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, - user_id: user_id + activity: %{data: %{"type" => activity_type}} = activity, + user: %User{id: user_id} } = notif ) when activity_type in @types do @@ -39,18 +39,17 @@ def perform( for subscription <- fetch_subsriptions(user_id), get_in(subscription.data, ["alerts", type]) do %{ - title: format_title(notif), access_token: subscription.token.token, - body: format_body(notif, actor, object), notification_id: notif.id, notification_type: type, icon: avatar_url, preferred_locale: "en", pleroma: %{ - activity_id: activity_id, + activity_id: notif.activity.id, direct_conversation_id: direct_conversation_id } } + |> Map.merge(build_content(notif, actor, object)) |> Jason.encode!() |> push_message(build_sub(subscription), gcm_api_key, subscription) end @@ -100,6 +99,24 @@ def build_sub(subscription) do } end + def build_content( + %{ + activity: %{data: %{"directMessage" => true}}, + user: %{notification_settings: %{privacy_option: true}} + }, + actor, + _ + ) do + %{title: "New Direct Message", body: "@#{actor.nickname}"} + end + + def build_content(notif, actor, object) do + %{ + title: format_title(notif), + body: format_body(notif, actor, object) + } + end + def format_body( %{activity: %{data: %{"type" => "Create"}}}, actor, diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 33b24840d..a1b445f2f 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,16 +129,17 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - blocks = user.blocks || [] - mutes = user.mutes || [] - reblog_mutes = user.muted_reblogs || [] - recipient_blocks = MapSet.new(blocks ++ mutes) + %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = + User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) + + recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipients = MapSet.new(item.recipients) domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) with parent <- Object.normalize(item) || item, - true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), - true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + true <- + Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)), + true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- MapSet.disjoint?(recipients, recipient_blocks), %{host: item_host} <- URI.parse(item.actor), %{host: parent_host} <- URI.parse(parent.data["actor"]), diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 7ffc8eabe..323a4da1e 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -71,4 +71,11 @@ def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, activity = Activity.get_by_id(activity_id) Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity) end + + def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do + origin = User.get_cached_by_id(origin_id) + target = User.get_cached_by_id(target_id) + + Pleroma.FollowingRelationship.move_following(origin, target) + end end diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 61b451e3e..a978c4013 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -13,7 +13,7 @@ def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) d notification = Notification |> Repo.get(notification_id) - |> Repo.preload([:activity]) + |> Repo.preload([:activity, :user]) Pleroma.Web.Push.Impl.perform(notification) end diff --git a/mix.exs b/mix.exs index 60c7fe3f6..7c8e52a67 100644 --- a/mix.exs +++ b/mix.exs @@ -100,9 +100,10 @@ defp deps do {:plug_cowboy, "~> 2.0"}, {:phoenix_pubsub, "~> 1.1"}, {:phoenix_ecto, "~> 4.0"}, + {:ecto_enum, "~> 1.4"}, {:ecto_sql, "~> 3.2"}, {:postgrex, ">= 0.13.5"}, - {:oban, "~> 0.8.1"}, + {:oban, "~> 0.12.0"}, {:quantum, "~> 2.3"}, {:gettext, "~> 0.15"}, {:comeonin, "~> 4.1.1"}, diff --git a/mix.lock b/mix.lock index 3a664287c..b008f2b55 100644 --- a/mix.lock +++ b/mix.lock @@ -23,8 +23,9 @@ "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "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"}, + "ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -38,7 +39,7 @@ "fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "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"}, - "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, + "floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, @@ -67,7 +68,7 @@ "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, @@ -97,7 +98,7 @@ "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, - "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, 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", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/priv/repo/migrations/20191025081729_add_move_support_to_users.exs b/priv/repo/migrations/20191025081729_add_move_support_to_users.exs new file mode 100644 index 000000000..580b9eb0f --- /dev/null +++ b/priv/repo/migrations/20191025081729_add_move_support_to_users.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddMoveSupportToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:also_known_as, {:array, :string}, default: [], null: false) + add(:allow_following_move, :boolean, default: true, null: false) + end + end +end diff --git a/priv/repo/migrations/20191118084425_create_user_relationships.exs b/priv/repo/migrations/20191118084425_create_user_relationships.exs new file mode 100644 index 000000000..c281f887d --- /dev/null +++ b/priv/repo/migrations/20191118084425_create_user_relationships.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.CreateUserRelationships do + use Ecto.Migration + + def change do + create_if_not_exists table(:user_relationships) do + add(:source_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:target_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:relationship_type, :integer, null: false) + + timestamps(updated_at: false) + end + + create_if_not_exists( + unique_index(:user_relationships, [:source_id, :relationship_type, :target_id]) + ) + end +end diff --git a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs new file mode 100644 index 000000000..990e9f3b8 --- /dev/null +++ b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs @@ -0,0 +1,68 @@ +defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do + use Ecto.Migration + + alias Ecto.Adapters.SQL + alias Pleroma.Repo + + require Logger + + def up do + Enum.each( + [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4, subscribers: 5], + fn {field, relationship_type_code} -> + migrate(field, relationship_type_code) + + if field == :subscribers do + drop_if_exists(index(:users, [:subscribers])) + end + end + ) + end + + def down, do: :noop + + defp migrate(field, relationship_type_code) do + Logger.info("Processing users.#{field}...") + + {:ok, %{rows: field_rows}} = + SQL.query(Repo, "SELECT id, #{field} FROM users WHERE #{field} != '{}'") + + target_ap_ids = + Enum.flat_map( + field_rows, + fn [_, ap_ids] -> ap_ids end + ) + |> Enum.uniq() + + # Selecting ids of all targets at once in order to reduce the number of SELECT queries + {:ok, %{rows: target_ap_id_id}} = + SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [target_ap_ids]) + + target_id_by_ap_id = Enum.into(target_ap_id_id, %{}, fn [k, v] -> {k, v} end) + + Enum.each( + field_rows, + fn [source_id, target_ap_ids] -> + source_uuid = Ecto.UUID.cast!(source_id) + + for target_ap_id <- target_ap_ids do + target_id = target_id_by_ap_id[target_ap_id] + + with {:ok, target_uuid} <- target_id && Ecto.UUID.cast(target_id) do + execute(""" + INSERT INTO user_relationships( + source_id, target_id, relationship_type, inserted_at + ) + VALUES( + '#{source_uuid}'::uuid, '#{target_uuid}'::uuid, #{relationship_type_code}, now() + ) + ON CONFLICT (source_id, relationship_type, target_id) DO NOTHING + """) + else + _ -> Logger.warn("Unresolved #{field} reference: (#{source_uuid}, #{target_id})") + end + end + end + ) + end +end diff --git a/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs b/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs new file mode 100644 index 000000000..76d3b32c4 --- /dev/null +++ b/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddActivitypubActorType do + use Ecto.Migration + + def change do + alter table("users") do + add(:actor_type, :string, null: false, default: "Person") + end + end +end diff --git a/priv/repo/migrations/20191123103423_remove_info_from_users.exs b/priv/repo/migrations/20191123103423_remove_info_from_users.exs new file mode 100644 index 000000000..b251255ea --- /dev/null +++ b/priv/repo/migrations/20191123103423_remove_info_from_users.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.RemoveInfoFromUsers do + use Ecto.Migration + + def change do + alter table(:users) do + remove(:info, :map, default: %{}) + end + end +end diff --git a/priv/repo/migrations/20191128153944_fix_missing_following_count.exs b/priv/repo/migrations/20191128153944_fix_missing_following_count.exs new file mode 100644 index 000000000..3236de7a4 --- /dev/null +++ b/priv/repo/migrations/20191128153944_fix_missing_following_count.exs @@ -0,0 +1,53 @@ +defmodule Pleroma.Repo.Migrations.FixMissingFollowingCount do + use Ecto.Migration + + def up do + """ + UPDATE + users + SET + following_count = sub.count + FROM + ( + SELECT + users.id AS sub_id + ,COUNT (following_relationships.id) + FROM + following_relationships + ,users + WHERE + users.id = following_relationships.follower_id + AND following_relationships.state = 'accept' + GROUP BY + users.id + ) AS sub + WHERE + users.id = sub.sub_id + AND users.local = TRUE + ; + """ + |> execute() + + """ + UPDATE + users + SET + following_count = 0 + WHERE + following_count IS NULL + """ + |> execute() + + execute("ALTER TABLE users + ALTER COLUMN following_count SET DEFAULT 0, + ALTER COLUMN following_count SET NOT NULL + ") + end + + def down do + execute("ALTER TABLE users + ALTER COLUMN following_count DROP DEFAULT, + ALTER COLUMN following_count DROP NOT NULL + ") + end +end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex new file mode 100644 index 000000000..ea0480dcd --- /dev/null +++ b/priv/scrubbers/default.ex @@ -0,0 +1,93 @@ +defmodule Pleroma.HTML.Scrubber.Default do + @doc "The default HTML scrubbing policy: no " + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + # credo:disable-for-previous-line + # No idea how to fix this one… + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + Meta.strip_comments() + + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "class", [ + "hashtag", + "u-url", + "mention", + "u-url mention", + "mention u-url" + ]) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer", + "ugc" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + + Meta.allow_tag_with_these_attributes(:abbr, ["title"]) + + Meta.allow_tag_with_these_attributes(:b, []) + Meta.allow_tag_with_these_attributes(:blockquote, []) + Meta.allow_tag_with_these_attributes(:br, []) + Meta.allow_tag_with_these_attributes(:code, []) + Meta.allow_tag_with_these_attributes(:del, []) + Meta.allow_tag_with_these_attributes(:em, []) + Meta.allow_tag_with_these_attributes(:i, []) + Meta.allow_tag_with_these_attributes(:li, []) + Meta.allow_tag_with_these_attributes(:ol, []) + Meta.allow_tag_with_these_attributes(:p, []) + Meta.allow_tag_with_these_attributes(:pre, []) + Meta.allow_tag_with_these_attributes(:strong, []) + Meta.allow_tag_with_these_attributes(:sub, []) + Meta.allow_tag_with_these_attributes(:sup, []) + Meta.allow_tag_with_these_attributes(:u, []) + Meta.allow_tag_with_these_attributes(:ul, []) + + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) + Meta.allow_tag_with_these_attributes(:span, []) + + @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) + + if @allow_inline_images do + # restrict img tags to http/https only, because of MediaProxy. + Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + + Meta.allow_tag_with_these_attributes(:img, [ + "width", + "height", + "class", + "title", + "alt" + ]) + end + + if Pleroma.Config.get([:markup, :allow_tables]) do + Meta.allow_tag_with_these_attributes(:table, []) + Meta.allow_tag_with_these_attributes(:tbody, []) + Meta.allow_tag_with_these_attributes(:td, []) + Meta.allow_tag_with_these_attributes(:th, []) + Meta.allow_tag_with_these_attributes(:thead, []) + Meta.allow_tag_with_these_attributes(:tr, []) + end + + if Pleroma.Config.get([:markup, :allow_headings]) do + Meta.allow_tag_with_these_attributes(:h1, []) + Meta.allow_tag_with_these_attributes(:h2, []) + Meta.allow_tag_with_these_attributes(:h3, []) + Meta.allow_tag_with_these_attributes(:h4, []) + Meta.allow_tag_with_these_attributes(:h5, []) + end + + if Pleroma.Config.get([:markup, :allow_fonts]) do + Meta.allow_tag_with_these_attributes(:font, ["face"]) + end + + Meta.strip_everything_not_covered() +end diff --git a/priv/scrubbers/links_only.ex b/priv/scrubbers/links_only.ex new file mode 100644 index 000000000..b30a00589 --- /dev/null +++ b/priv/scrubbers/links_only.ex @@ -0,0 +1,27 @@ +defmodule Pleroma.HTML.Scrubber.LinksOnly do + @moduledoc """ + An HTML scrubbing policy which limits to links only. + """ + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + Meta.strip_comments() + + # links + Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer", + "me", + "ugc" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + Meta.strip_everything_not_covered() +end diff --git a/priv/scrubbers/media_proxy.ex b/priv/scrubbers/media_proxy.ex new file mode 100644 index 000000000..5dbe57666 --- /dev/null +++ b/priv/scrubbers/media_proxy.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.HTML.Transform.MediaProxy do + @moduledoc "Transforms inline image URIs to use MediaProxy." + + alias Pleroma.Web.MediaProxy + + def before_scrub(html), do: html + + def scrub_attribute(:img, {"src", "http" <> target}) do + media_url = + ("http" <> target) + |> MediaProxy.url() + + {"src", media_url} + end + + def scrub_attribute(_tag, attribute), do: attribute + + def scrub({:img, attributes, children}) do + attributes = + attributes + |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) + |> Enum.reject(&is_nil(&1)) + + {:img, attributes, children} + end + + def scrub({:comment, _text, _children}), do: "" + + def scrub({tag, attributes, children}), do: {tag, attributes, children} + def scrub({_tag, children}), do: children + def scrub(text), do: text +end diff --git a/priv/scrubbers/twitter_text.ex b/priv/scrubbers/twitter_text.ex new file mode 100644 index 000000000..c4e796cad --- /dev/null +++ b/priv/scrubbers/twitter_text.ex @@ -0,0 +1,57 @@ +defmodule Pleroma.HTML.Scrubber.TwitterText do + @moduledoc """ + An HTML scrubbing policy which limits to twitter-style text. Only + paragraphs, breaks and links are allowed through the filter. + """ + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + Meta.strip_comments() + + # links + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "class", [ + "hashtag", + "u-url", + "mention", + "u-url mention", + "mention u-url" + ]) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + + # paragraphs and linebreaks + Meta.allow_tag_with_these_attributes(:br, []) + Meta.allow_tag_with_these_attributes(:p, []) + + # microformats + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) + Meta.allow_tag_with_these_attributes(:span, []) + + # allow inline images for custom emoji + if Pleroma.Config.get([:markup, :allow_inline_images]) do + # restrict img tags to http/https only, because of MediaProxy. + Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + + Meta.allow_tag_with_these_attributes(:img, [ + "width", + "height", + "class", + "title", + "alt" + ]) + end + + Meta.strip_everything_not_covered() +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 8bae42f6d..e7ebf72be 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -29,7 +29,11 @@ "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" }, - "EmojiReaction": "litepub:EmojiReaction" + "EmojiReaction": "litepub:EmojiReaction", + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } } ] } diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index 863270022..9b2c97963 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -252,7 +252,7 @@ test "when the user blocks a recipient, the existing conversations with them are assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) # The conversations with the blocked user are marked as read assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = @@ -274,7 +274,7 @@ test "the new conversation with the blocked user is not marked as unread " do blocked = insert(:user) third_user = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) # When the blocked user is the author {:ok, _direct1} = @@ -311,7 +311,7 @@ test "the conversation with the blocked user is not marked as unread on a reply" "visibility" => "direct" }) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) assert [%{read: true}] = Participation.for_user(blocker) assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs new file mode 100644 index 000000000..45800568a --- /dev/null +++ b/test/federation/federation_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.FederationTest do + use Pleroma.DataCase + @moduletag :federated + import Pleroma.Cluster + + setup_all do + Pleroma.Cluster.spawn_default_cluster() + :ok + end + + @federated1 :"federated1@127.0.0.1" + describe "federated cluster primitives" do + test "within/2 captures local bindings and executes block on remote node" do + captured_binding = :captured + + result = + within @federated1 do + user = Pleroma.Factory.insert(:user) + {captured_binding, node(), user} + end + + assert {:captured, @federated1, user} = result + refute Pleroma.User.get_by_id(user.id) + assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id + end + + test "runs webserver on customized port" do + {nickname, url, url_404} = + within @federated1 do + import Pleroma.Web.Router.Helpers + user = Pleroma.Factory.insert(:user) + user_url = account_url(Pleroma.Web.Endpoint, :show, user) + url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists") + + {user.nickname, user_url, url_404} + end + + assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}") + assert %{"acct" => ^nickname} = Jason.decode!(body) + assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}") + end + end +end diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index 8159dc20a..9fdd6557c 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -9,7 +9,11 @@ "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "toot": "http://joinmastodon.org/ns#", - "Emoji": "toot:Emoji" + "Emoji": "toot:Emoji", + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } }], "id": "http://mastodon.example.org/users/admin", "type": "Person", @@ -50,5 +54,6 @@ "type": "Image", "mediaType": "image/png", "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } + }, + "alsoKnownAs": ["http://example.org/users/foo"] } diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json new file mode 100644 index 000000000..7b86b5fe2 --- /dev/null +++ b/test/fixtures/users_mock/friendica_followers.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "vcard": "http://www.w3.org/2006/vcard/ns#", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "directMessage": "litepub:directMessage" + } + ], + "id": "http://localhost:8080/followers/fuser3", + "type": "OrderedCollection", + "totalItems": 296 +} diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json new file mode 100644 index 000000000..7c526befc --- /dev/null +++ b/test/fixtures/users_mock/friendica_following.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "vcard": "http://www.w3.org/2006/vcard/ns#", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "directMessage": "litepub:directMessage" + } + ], + "id": "http://localhost:8080/following/fuser3", + "type": "OrderedCollection", + "totalItems": 32 +} diff --git a/test/html_test.exs b/test/html_test.exs index f0869534c..c918dbe20 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -228,5 +228,16 @@ test "skips microformats hashtags" do assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end + + test "does not crash when there is an HTML entity in a link" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "\"http://cofe.com/?boomer=ok&foo=bar\""}) + + object = Object.normalize(activity) + + assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) + end end end diff --git a/test/notification_test.exs b/test/notification_test.exs index f8d429223..ffa3d4b8c 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -93,7 +93,7 @@ test "it creates a notification for user if the user blocks the activity author" activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) user = insert(:user) - {:ok, user} = User.block(user, author) + {:ok, _user_relationship} = User.block(user, author) assert Notification.create_notification(activity, user) end @@ -112,7 +112,7 @@ test "notification created if user is muted without notifications" do muter = insert(:user) muted = insert(:user) - {:ok, muter} = User.mute(muter, muted, false) + {:ok, _user_relationships} = User.mute(muter, muted, false) {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"}) @@ -136,7 +136,10 @@ test "it creates a notification for an activity from a muted thread" do test "it disables notifications from followers" do follower = insert(:user) - followed = insert(:user, notification_settings: %{"followers" => false}) + + followed = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false}) + User.follow(follower, followed) {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) @@ -144,13 +147,20 @@ test "it disables notifications from followers" do test "it disables notifications from non-followers" do follower = insert(:user) - followed = insert(:user, notification_settings: %{"non_followers" => false}) + + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{non_followers: false} + ) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) end test "it disables notifications from people the user follows" do - follower = insert(:user, notification_settings: %{"follows" => false}) + follower = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false}) + followed = insert(:user) User.follow(follower, followed) follower = Repo.get(User, follower.id) @@ -159,7 +169,9 @@ test "it disables notifications from people the user follows" do end test "it disables notifications from people the user does not follow" do - follower = insert(:user, notification_settings: %{"non_follows" => false}) + follower = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false}) + followed = insert(:user) {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) refute Notification.create_notification(activity, follower) @@ -630,13 +642,46 @@ test "notifications are deleted if a remote user is deleted" do assert Enum.empty?(Notification.for_user(local_user)) end + + test "move activity generates a notification" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + other_follower = insert(:user, %{allow_following_move: false}) + + User.follow(follower, old_user) + User.follow(other_follower, old_user) + + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + ObanHelpers.perform_all() + + assert [] = Notification.for_user(follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(follower, %{with_move: true}) + + assert [] = Notification.for_user(other_follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(other_follower, %{with_move: true}) + end end describe "for_user" do test "it returns notifications for muted user without notifications" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted, false) + {:ok, _user_relationships} = User.mute(user, muted, false) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -646,7 +691,7 @@ test "it returns notifications for muted user without notifications" do test "it doesn't return notifications for muted user with notifications" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted) + {:ok, _user_relationships} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -656,7 +701,7 @@ test "it doesn't return notifications for muted user with notifications" do test "it doesn't return notifications for blocked user" do user = insert(:user) blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) @@ -686,7 +731,7 @@ test "it doesn't return notifications for muted thread" do test "it returns notifications from a muted user when with_muted is set" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted) + {:ok, _user_relationships} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -696,7 +741,7 @@ test "it returns notifications from a muted user when with_muted is set" do test "it doesn't return notifications from a blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 136dcc54e..bc6fcd73c 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -8,36 +8,116 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do alias Pleroma.Plugs.UserIsAdminPlug import Pleroma.Factory - test "accepts a user that is admin" do - user = insert(:user, is_admin: true) + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end - conn = - build_conn() - |> assign(:user, user) + test "accepts a user that is an admin" do + user = insert(:user, is_admin: true) - ret_conn = - conn - |> UserIsAdminPlug.call(%{}) + conn = assign(build_conn(), :user, user) - assert conn == ret_conn + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "denies a user that isn't an admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) + + assert conn.status == 403 + end end - test "denies a user that isn't admin" do - user = insert(:user) + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) + setup do + admin_user = insert(:user, is_admin: true) + non_admin_user = insert(:user, is_admin: false) + blank_user = nil - assert conn.status == 403 - end + {:ok, %{users: [admin_user, non_admin_user, blank_user]}} + end - test "denies when a user isn't set" do - conn = - build_conn() - |> UserIsAdminPlug.call(%{}) + test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do + user = insert(:user, is_admin: true) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) - assert conn.status == 403 + conn = + conn + |> assign(:user, user) + |> assign(:token, token) + + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do + user = insert(:user, is_admin: false) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do + token = insert(:oauth_token, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, nil) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "if token lacks admin scopes, denies users regardless of is_admin flag", + %{users: users} do + for user <- users do + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end + + test "if token is missing, denies users regardless of is_admin flag", %{users: users} do + for user <- users do + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, nil) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end end end diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex index 6da16f71a..fcfea666f 100644 --- a/test/support/builders/user_builder.ex +++ b/test/support/builders/user_builder.ex @@ -10,7 +10,8 @@ def build(data \\ %{}) do password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: "A tester.", ap_id: "some id", - last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + notification_settings: %Pleroma.User.NotificationSetting{} } Map.merge(user, data) diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index 466d8986f..4a4585844 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChannelCase do quote do # Import conveniences for testing with channels use Phoenix.ChannelTest + use Pleroma.Tests.Helpers # The default endpoint for testing @endpoint Pleroma.Web.Endpoint diff --git a/test/support/cluster.ex b/test/support/cluster.ex new file mode 100644 index 000000000..deb37f361 --- /dev/null +++ b/test/support/cluster.ex @@ -0,0 +1,218 @@ +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_slave(&1) end)) + |> Enum.map(&Task.await(&1, 60_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_slave({node_host, override_configs}) do + log(node_host, "booting federated VM") + {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), 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 diff --git a/test/support/factory.ex b/test/support/factory.ex index e3f797f64..314f26ec9 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -31,8 +31,8 @@ def user_factory do nickname: sequence(:nickname, &"nick#{&1}"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: sequence(:bio, &"Tester Number #{&1}"), - info: %{}, - last_digest_emailed_at: NaiveDateTime.utc_now() + last_digest_emailed_at: NaiveDateTime.utc_now(), + notification_settings: %Pleroma.User.NotificationSetting{} } %{ @@ -43,6 +43,18 @@ def user_factory do } end + def user_relationship_factory(attrs \\ %{}) do + source = attrs[:source] || insert(:user) + target = attrs[:target] || insert(:user) + relationship_type = attrs[:relationship_type] || :block + + %Pleroma.UserRelationship{ + source_id: source.id, + target_id: target.id, + relationship_type: relationship_type + } + end + def note_factory(attrs \\ %{}) do text = sequence(:text, &"This is :moominmamma: note #{&1}") diff --git a/test/support/helpers.ex b/test/support/helpers.ex index ce39dd9d8..af2b2eddf 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -75,6 +75,23 @@ def render_json(view, template, assigns) do |> Poison.decode!() end + def stringify_keys(nil), do: nil + + def stringify_keys(key) when key in [true, false], do: key + def stringify_keys(key) when is_atom(key), do: Atom.to_string(key) + + def stringify_keys(map) when is_map(map) do + map + |> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end) + |> Enum.into(%{}) + end + + def stringify_keys([head | rest] = list) when is_list(list) do + [stringify_keys(head) | stringify_keys(rest)] + end + + def stringify_keys(key), do: key + defmacro guards_config(config_path) do quote do initial_setting = Pleroma.Config.get(config_path) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 965335e96..e3a621f49 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1035,6 +1035,22 @@ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do }} end + def get("http://localhost:8080/followers/fuser3", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/friendica_followers.json") + }} + end + + def get("http://localhost:8080/following/fuser3", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/friendica_following.json") + }} + end + def get("http://localhost:4001/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index 9cd47380c..fab9d6e9a 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -63,4 +63,84 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," end + + test "load a settings with large values and pass to file", %{temp_file: temp_file} do + Config.create(%{ + group: "pleroma", + key: ":instance", + value: [ + name: "Pleroma", + email: "example@example.com", + notify_email: "noreply@example.com", + description: "A Pleroma instance, an alternative fediverse server", + limit: 5_000, + chat_limit: 5_000, + remote_limit: 100_000, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, + registrations_open: true, + federating: true, + federation_incoming_replies_max_depth: 100, + federation_reachability_timeout_days: 7, + federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], + allow_relay: true, + rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, + public: true, + quarantined_instances: [], + managed_config: true, + static_dir: "instance/static/", + allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + mrf_transparency: true, + mrf_transparency_exclusions: [], + autofollowed_nicknames: [], + max_pinned_statuses: 1, + no_attachment_links: true, + welcome_user_nickname: nil, + welcome_message: nil, + max_report_comment_size: 1000, + safe_dm_mentions: false, + healthcheck: false, + remote_post_retention_days: 90, + skip_thread_containment: true, + limit_to_local_content: :unauthenticated, + dynamic_configuration: false, + user_bio_length: 5000, + user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 2048, + external_user_synchronization: true, + extended_nickname_format: true, + multi_factor_authentication: [ + totp: [ + # digits 6 or 8 + digits: 6, + period: 30 + ], + backup_codes: [ + number: 2, + length: 6 + ] + ] + ] + }) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"]) + + assert Repo.all(Config) == [] + assert File.exists?(temp_file) + {:ok, file} = File.read(temp_file) + + assert file == + "use Mix.Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n no_attachment_links: true,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n dynamic_configuration: false,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index c8dbee010..241ad1f94 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,7 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] -ExUnit.start(exclude: os_exclude) +ExUnit.start(exclude: [:federated | os_exclude]) + Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client) {:ok, _} = Application.ensure_all_started(:ex_machina) diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs new file mode 100644 index 000000000..4744d7b4a --- /dev/null +++ b/test/user/notification_setting_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSettingTest do + use Pleroma.DataCase + + alias Pleroma.User.NotificationSetting + + describe "changeset/2" do + test "sets valid privacy option" do + changeset = + NotificationSetting.changeset( + %NotificationSetting{}, + %{"privacy_option" => true} + ) + + assert %Ecto.Changeset{valid?: true} = changeset + end + end +end diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs new file mode 100644 index 000000000..437450bc3 --- /dev/null +++ b/test/user_relationship_test.exs @@ -0,0 +1,130 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationshipTest do + alias Pleroma.UserRelationship + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "*_exists?/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "returns false if record doesn't exist", %{users: [user1, user2]} do + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "returns true if record exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + end + + describe "create_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + UserRelationship.create_block(user1, user2) + UserRelationship.create_mute(user1, user2) + UserRelationship.create_notification_mute(user1, user2) + UserRelationship.create_reblog_mute(user1, user2) + UserRelationship.create_inverse_subscription(user1, user2) + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record already exists, returns it", %{users: [user1, user2]} do + user_block = UserRelationship.create_block(user1, user2) + assert user_block == UserRelationship.create_block(user1, user2) + end + end + + describe "delete_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "deletes user relationship record if it exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) + + assert {:ok, %UserRelationship{}} = + UserRelationship.delete_inverse_subscription(user1, user2) + + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do + assert {:ok, nil} = UserRelationship.delete_block(user1, user2) + assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) + end + end +end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 98841dbbd..821858476 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -174,6 +174,7 @@ test "works with URIs" do |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) |> Map.put(:last_digest_emailed_at, nil) + |> Map.put(:notification_settings, nil) assert user == expected end diff --git a/test/user_test.exs b/test/user_test.exs index 6ae563258..d7ab63463 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -44,6 +44,56 @@ test "returns invisible actor" do end end + describe "AP ID user relationships" do + setup do + {:ok, user: insert(:user)} + end + + test "outgoing_relations_ap_ids/1", %{user: user} do + rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] + + ap_ids_by_rel = + Enum.into( + rel_types, + %{}, + fn rel_type -> + rel_records = + insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) + + ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) + {rel_type, Enum.sort(ap_ids)} + end + ) + + assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) + assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) + assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(User.notification_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:reblog_mute] == + Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) + + assert ap_ids_by_rel[:inverse_subscription] == + Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) + + outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types) + + assert ap_ids_by_rel == + Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) + end + end + describe "when tags are nil" do test "tagging a user" do user = insert(:user, %{tags: nil}) @@ -119,7 +169,7 @@ test "clears follow requests when requester is blocked" do CommonAPI.follow(follower, followed) assert [_activity] = User.get_follow_requests(followed) - {:ok, _follower} = User.block(followed, follower) + {:ok, _user_relationship} = User.block(followed, follower) assert [] = User.get_follow_requests(followed) end @@ -132,8 +182,8 @@ test "follow_all follows mutliple users" do not_followed = insert(:user) reverse_blocked = insert(:user) - {:ok, user} = User.block(user, blocked) - {:ok, reverse_blocked} = User.block(reverse_blocked, user) + {:ok, _user_relationship} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(reverse_blocked, user) {:ok, user} = User.follow(user, followed_zero) @@ -186,7 +236,7 @@ test "can't follow a user who blocked us" do blocker = insert(:user) blockee = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:error, _} = User.follow(blockee, blocker) end @@ -195,7 +245,7 @@ test "can't subscribe to a user who blocked us" do blocker = insert(:user) blocked = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) {:error, _} = User.subscribe(blocked, blocker) end @@ -367,18 +417,6 @@ test "it sets the password_hash and ap_id" do assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end - - test "it ensures info is not nil" do - changeset = User.register_changeset(%User{}, @full_user_data) - - assert changeset.valid? - - {:ok, user} = - changeset - |> Repo.insert() - - refute is_nil(user.info) - end end describe "user registration, with :account_activation_required" do @@ -432,8 +470,7 @@ test "gets an existing user by ap_id" do :user, local: false, nickname: "admin@mastodon.example.org", - ap_id: ap_id, - info: %{} + ap_id: ap_id ) {:ok, fetched_user} = User.get_or_fetch(ap_id) @@ -494,8 +531,7 @@ test "updates an existing user, if stale" do local: false, nickname: "admin@mastodon.example.org", ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: a_week_ago, - info: %{} + last_refreshed_at: a_week_ago ) assert orig_user.last_refreshed_at == a_week_ago @@ -536,7 +572,6 @@ test "returns an ap_followers link for a user" do name: "Someone", nickname: "a@b.de", ap_id: "http...", - info: %{some: "info"}, avatar: %{some: "avatar"} } @@ -693,7 +728,7 @@ test "it mutes people" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, user} = User.mute(user, muted_user) + {:ok, _user_relationships} = User.mute(user, muted_user) assert User.mutes?(user, muted_user) assert User.muted_notifications?(user, muted_user) @@ -703,8 +738,8 @@ test "it unmutes users" do user = insert(:user) muted_user = insert(:user) - {:ok, user} = User.mute(user, muted_user) - {:ok, user} = User.unmute(user, muted_user) + {:ok, _user_relationships} = User.mute(user, muted_user) + {:ok, _user_mute} = User.unmute(user, muted_user) refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) @@ -717,7 +752,7 @@ test "it mutes user without notifications" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, user} = User.mute(user, muted_user, false) + {:ok, _user_relationships} = User.mute(user, muted_user, false) assert User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) @@ -731,7 +766,7 @@ test "it blocks people" do refute User.blocks?(user, blocked_user) - {:ok, user} = User.block(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) assert User.blocks?(user, blocked_user) end @@ -740,8 +775,8 @@ test "it unblocks users" do user = insert(:user) blocked_user = insert(:user) - {:ok, user} = User.block(user, blocked_user) - {:ok, user} = User.unblock(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) + {:ok, _user_block} = User.unblock(user, blocked_user) refute User.blocks?(user, blocked_user) end @@ -756,7 +791,7 @@ test "blocks tear down cyclical follow relationships" do assert User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -774,7 +809,7 @@ test "blocks tear down blocker->blocked follow relationships" do assert User.following?(blocker, blocked) refute User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -792,7 +827,7 @@ test "blocks tear down blocked->blocker follow relationships" do refute User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -805,12 +840,12 @@ test "blocks tear down blocked->blocker subscription relationships" do blocker = insert(:user) blocked = insert(:user) - {:ok, blocker} = User.subscribe(blocked, blocker) + {:ok, _subscription} = User.subscribe(blocked, blocker) assert User.subscribed_to?(blocked, blocker) refute User.subscribed_to?(blocker, blocked) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) assert User.blocks?(blocker, blocked) refute User.subscribed_to?(blocker, blocked) @@ -1153,8 +1188,7 @@ test "with an overly long bio" do ap_id: user.ap_id, name: user.name, nickname: user.nickname, - bio: String.duplicate("h", current_max_length + 1), - info: %{} + bio: String.duplicate("h", current_max_length + 1) } assert {:ok, %User{}} = User.insert_or_update_user(data) @@ -1167,8 +1201,7 @@ test "with an overly long display name" do data = %{ ap_id: user.ap_id, name: String.duplicate("h", current_max_length + 1), - nickname: user.nickname, - info: %{} + nickname: user.nickname } assert {:ok, %User{}} = User.insert_or_update_user(data) @@ -1351,7 +1384,8 @@ test "follower count is updated when a follower is blocked" do {:ok, _follower2} = User.follow(follower2, user) {:ok, _follower3} = User.follow(follower3, user) - {:ok, user} = User.block(user, follower) + {:ok, _user_relationship} = User.block(user, follower) + user = refresh_record(user) assert user.follower_count == 2 end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 1aa73d75c..ba2ce1dd9 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -298,7 +298,7 @@ test "cached purged after activity deletion", %{conn: conn} do assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - Activity.delete_by_ap_id(activity.object.data["id"]) + Activity.delete_all_by_object_ap_id(activity.object.data["id"]) conn2 = conn diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 8b45a186b..8ae0f45d0 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -4,8 +4,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Activity alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -484,7 +487,7 @@ test "retrieves activities that have a given context" do activity_five = insert(:note_activity) user = insert(:user) - {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) assert activities == [activity_two, activity] @@ -497,7 +500,7 @@ test "doesn't return blocked activities" do activity_three = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) activities = ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -506,7 +509,7 @@ test "doesn't return blocked activities" do assert Enum.member?(activities, activity_three) refute Enum.member?(activities, activity_one) - {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) + {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) activities = ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -515,7 +518,7 @@ test "doesn't return blocked activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) @@ -542,7 +545,7 @@ test "doesn't return transitive interactions concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) @@ -565,7 +568,7 @@ test "doesn't return announce activities concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) @@ -644,7 +647,9 @@ test "doesn't return muted activities" do activity_three = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]}) + + activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_one_actor) activities = ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -665,7 +670,7 @@ test "doesn't return muted activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]}) + {:ok, _user_mute} = User.unmute(user, activity_one_actor) activities = ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -674,7 +679,8 @@ test "doesn't return muted activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]}) + activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_three_actor) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) @@ -821,7 +827,7 @@ test "doesn't return reblogs for users for whom reblogs have been muted" do activity = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) @@ -834,8 +840,8 @@ test "returns reblogs for users for whom reblogs have not been muted" do activity = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = CommonAPI.hide_reblogs(user, booster) - {:ok, user} = CommonAPI.show_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) @@ -1286,6 +1292,21 @@ test "decreases reply count" do assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 0 end + + test "it passes delete activity through MRF before deleting the object" do + rewrite_policy = Pleroma.Config.get([:instance, :rewrite_policy]) + Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy) + + on_exit(fn -> Pleroma.Config.put([:instance, :rewrite_policy], rewrite_policy) end) + + note = insert(:note_activity) + object = Object.normalize(note) + + {:error, {:reject, _}} = ActivityPub.delete(object) + + assert Activity.get_by_id(note.id) + assert Repo.get(Object, object.id).data["type"] == object.data["type"] + end end describe "timeline post-processing" do @@ -1587,5 +1608,80 @@ test "detects hidden follows" do assert follow_info.hide_followers == false assert follow_info.hide_follows == true end + + test "detects hidden follows/followers for friendica" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:8080/followers/fuser3", + following_address: "http://localhost:8080/following/fuser3" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.follower_count == 296 + assert follow_info.following_count == 32 + assert follow_info.hide_follows == true + end + end + + describe "Move activity" do + test "create" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + follower_move_opted_out = insert(:user, allow_following_move: false) + + User.follow(follower, old_user) + User.follow(follower_move_opted_out, old_user) + + assert User.following?(follower, old_user) + assert User.following?(follower_move_opted_out, old_user) + + assert {:ok, activity} = ActivityPub.move(old_user, new_user) + + assert %Activity{ + actor: ^old_ap_id, + data: %{ + "actor" => ^old_ap_id, + "object" => ^old_ap_id, + "target" => ^new_ap_id, + "type" => "Move" + }, + local: true + } = activity + + params = %{ + "op" => "move_following", + "origin_id" => old_user.id, + "target_id" => new_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(params, nil) + + refute User.following?(follower, old_user) + assert User.following?(follower, new_user) + + assert User.following?(follower_move_opted_out, old_user) + refute User.following?(follower_move_opted_out, new_user) + + activity = %Activity{activity | object: nil} + + assert [%Notification{activity: ^activity}] = + Notification.for_user(follower, %{with_move: true}) + + assert [%Notification{activity: ^activity}] = + Notification.for_user(follower_move_opted_out, %{with_move: true}) + end + + test "old user must be in the new user's `also_known_as` list" do + old_user = insert(:user) + new_user = insert(:user) + + assert {:error, "Target account must have the origin in `alsoKnownAs`"} = + ActivityPub.move(old_user, new_user) + end end end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 75cfbea2e..7d6d0814d 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -128,7 +128,7 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl user = insert(:user) {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") - {:ok, user} = User.block(user, target) + {:ok, _user_relationship} = User.block(user, target) data = File.read!("test/fixtures/mastodon-follow-activity.json") diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 0bdd514e9..5da358c43 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -39,6 +39,7 @@ test "it ignores an incoming notice if we already have it" do assert activity == returned_activity end + @tag capture_log: true test "it fetches replied-to activities if we don't have them" do data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -533,6 +534,7 @@ test "it works for incoming announces with an inlined activity" do assert object.data["content"] == "this is a private toot" end + @tag capture_log: true test "it rejects incoming announces with an inlined activity from another origin" do data = File.read!("test/fixtures/bogus-mastodon-announce.json") @@ -681,6 +683,37 @@ test "it works for incoming update activities" do assert user.bio == "

Some bio

" end + test "it works with alsoKnownAs" do + {:ok, %Activity{data: %{"actor" => actor}}} = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"] + + {:ok, _activity} = + "test/fixtures/mastodon-update.json" + |> File.read!() + |> Poison.decode!() + |> Map.put("actor", actor) + |> Map.update!("object", fn object -> + object + |> Map.put("actor", actor) + |> Map.put("id", actor) + |> Map.put("alsoKnownAs", [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ]) + end) + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ] + end + test "it works with custom profile fields" do {:ok, activity} = "test/fixtures/mastodon-post-activity.json" @@ -814,6 +847,7 @@ test "it fails for incoming deletes with spoofed origin" do assert Activity.get_by_id(activity.id) end + @tag capture_log: true test "it works for incoming user deletes" do %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") @@ -1269,6 +1303,30 @@ test "it correctly processes messages with non-array cc field" do assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] assert [user.follower_address] == activity.data["to"] end + + test "it accepts Move activities" do + old_user = insert(:user) + new_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Move", + "actor" => old_user.ap_id, + "object" => old_user.ap_id, + "target" => new_user.ap_id + } + + assert :error = Transmogrifier.handle_incoming(message) + + {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) + + assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) + assert activity.actor == old_user.ap_id + assert activity.data["actor"] == old_user.ap_id + assert activity.data["object"] == old_user.ap_id + assert activity.data["target"] == new_user.ap_id + assert activity.data["type"] == "Move" + end end describe "prepare outgoing" do @@ -1749,6 +1807,7 @@ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: assert modified_object["inReplyToAtomUri"] == "" end + @tag capture_log: true test "returns modified object when allowed incoming reply", %{data: data} do object_with_reply = Map.put( @@ -1868,6 +1927,7 @@ test "returns nil when cannot normalize object" do end) =~ "Unsupported URI scheme" end + @tag capture_log: true test "returns {:ok, %Object{}} for success case" do assert {:ok, %Object{}} = Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873") diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index 1feb076ba..586eb1d2f 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -636,47 +636,4 @@ test "removes actor from announcements" do assert updated_object.data["announcement_count"] == 1 end end - - describe "get_reports_grouped_by_status/1" do - setup do - [reporter, target_user] = insert_pair(:user) - first_status = insert(:note_activity, user: target_user) - second_status = insert(:note_activity, user: target_user) - - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "comment" => "I feel offended", - "status_ids" => [first_status.id] - }) - - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "comment" => "I feel offended2", - "status_ids" => [second_status.id] - }) - - data = [%{activity: first_status.data["id"]}, %{activity: second_status.data["id"]}] - - {:ok, - %{ - first_status: first_status, - second_status: second_status, - data: data - }} - end - - test "works for deprecated reports format", %{ - first_status: first_status, - second_status: second_status, - data: data - } do - groups = Utils.get_reports_grouped_by_status(data).groups - - first_group = Enum.find(groups, &(&1.status.id == first_status.data["id"])) - second_group = Enum.find(groups, &(&1.status.id == second_status.data["id"])) - - assert first_group.status.id == first_status.data["id"] - assert second_group.status.id == second_status.data["id"] - end - end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index bb2ca6a62..23ca7f110 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -15,6 +15,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy import Pleroma.Factory @@ -24,6 +25,60 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok end + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end + + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end + + test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do + user = insert(:user) + admin = insert(:user, is_admin: true) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + end + end + describe "DELETE /api/pleroma/admin/users" do test "single user" do admin = insert(:user, is_admin: true) @@ -97,7 +152,7 @@ test "Create" do assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end - test "Cannot create user with exisiting email" do + test "Cannot create user with existing email" do admin = insert(:user, is_admin: true) user = insert(:user) @@ -128,7 +183,7 @@ test "Cannot create user with exisiting email" do ] end - test "Cannot create user with exisiting nickname" do + test "Cannot create user with existing nickname" do admin = insert(:user, is_admin: true) user = insert(:user) @@ -1559,7 +1614,8 @@ test "returns 403 when requested by a non-admin" do |> assign(:user, user) |> get("/api/pleroma/admin/reports") - assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin or OAuth admin scope is not granted."} end test "returns 403 when requested by anonymous" do @@ -1612,6 +1668,7 @@ test "returns 403 when requested by anonymous" do first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]), second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]), third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]), + first_report: first_report, first_status_reports: [first_report, second_report, third_report], second_status_reports: [first_report, second_report], third_status_reports: [first_report], @@ -1638,14 +1695,11 @@ test "returns reports grouped by status", %{ assert length(response["reports"]) == 3 - first_group = - Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"])) + first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) - second_group = - Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"])) + second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id)) - third_group = - Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"])) + third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id)) assert length(first_group["reports"]) == 3 assert length(second_group["reports"]) == 2 @@ -1656,13 +1710,14 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert first_group["status"] == %{ - "id" => first_status.data["id"], - "content" => first_status.object.data["content"], - "published" => first_status.object.data["published"] - } + assert first_group["status"] == + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: first_status})), + "deleted", + false + ) - assert first_group["account"]["id"] == target_user.id + assert(first_group["account"]["id"] == target_user.id) assert length(first_group["actors"]) == 1 assert hd(first_group["actors"])["id"] == reporter.id @@ -1675,11 +1730,12 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert second_group["status"] == %{ - "id" => second_status.data["id"], - "content" => second_status.object.data["content"], - "published" => second_status.object.data["published"] - } + assert second_group["status"] == + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: second_status})), + "deleted", + false + ) assert second_group["account"]["id"] == target_user.id @@ -1694,11 +1750,12 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert third_group["status"] == %{ - "id" => third_status.data["id"], - "content" => third_status.object.data["content"], - "published" => third_status.object.data["published"] - } + assert third_group["status"] == + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: third_status})), + "deleted", + false + ) assert third_group["account"]["id"] == target_user.id @@ -1708,6 +1765,70 @@ test "returns reports grouped by status", %{ assert Enum.map(third_group["reports"], & &1["id"]) -- Enum.map(third_status_reports, & &1.id) == [] end + + test "reopened report renders status data", %{ + conn: conn, + first_report: first_report, + first_status: first_status + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) + + assert first_group["status"] == + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: first_status})), + "deleted", + false + ) + end + + test "reopened report does not render status data if status has been deleted", %{ + conn: conn, + first_report: first_report, + first_status: first_status, + target_user: target_user + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + {:ok, _} = CommonAPI.delete(first_status.id, target_user) + + refute Activity.get_by_ap_id(first_status.id) + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][ + "deleted" + ] == true + + assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2 + end + + test "account not empty if status was deleted", %{ + conn: conn, + first_report: first_report, + first_status: first_status, + target_user: target_user + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + {:ok, _} = CommonAPI.delete(first_status.id, target_user) + + refute Activity.get_by_ap_id(first_status.id) + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"] + end end describe "POST /api/pleroma/admin/reports/:id/respond" do @@ -1923,6 +2044,7 @@ test "with settings in db", %{conn: conn} do Pleroma.Config.put([:instance, :dynamic_configuration], true) end + @tag capture_log: true test "create new config setting in db", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/config", %{ diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs new file mode 100644 index 000000000..68c24a9f9 --- /dev/null +++ b/test/web/chat_channel_test.exs @@ -0,0 +1,37 @@ +defmodule Pleroma.Web.ChatChannelTest do + use Pleroma.Web.ChannelCase + alias Pleroma.Web.ChatChannel + alias Pleroma.Web.UserSocket + + import Pleroma.Factory + + setup do + user = insert(:user) + + {:ok, _, socket} = + socket(UserSocket, "", %{user_name: user.nickname}) + |> subscribe_and_join(ChatChannel, "chat:public") + + {:ok, socket: socket} + end + + test "it broadcasts a message", %{socket: socket} do + push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) + assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) + end + + describe "message lengths" do + clear_config([:instance, :chat_limit]) + + test "it ignores messages of length zero", %{socket: socket} do + push(socket, "new_msg", %{"text" => ""}) + refute_broadcast("new_msg", %{text: ""}) + end + + test "it ignores messages above a certain length", %{socket: socket} do + Pleroma.Config.put([:instance, :chat_limit], 2) + push(socket, "new_msg", %{"text" => "123"}) + refute_broadcast("new_msg", %{text: "123"}) + end + end +end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 138488d44..b5d6d4055 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -509,14 +509,14 @@ test "updates state of multiple reports" do end test "add a reblog mute", %{muter: muter, muted: muted} do - {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == false end test "remove a reblog mute", %{muter: muter, muted: muted} do - {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) - {:ok, muter} = CommonAPI.show_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == true end @@ -526,7 +526,7 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do test "also unsubscribes a user" do [follower, followed] = insert_pair(:user) {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - {:ok, followed} = User.subscribe(follower, followed) + {:ok, _subscription} = User.subscribe(follower, followed) assert User.subscribed_to?(follower, followed) diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 519b56d6c..77cfce4fa 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -103,6 +103,21 @@ test "updates the user's locking status", %{conn: conn} do assert user["locked"] == true end + test "updates the user's allow_following_move", %{conn: conn} do + user = insert(:user) + + assert user.allow_following_move == true + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) + + assert refresh_record(user).allow_following_move == false + assert user = json_response(conn, 200) + assert user["pleroma"]["allow_following_move"] == false + end + test "updates the user's default scope", %{conn: conn} do user = insert(:user) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 585cb8a9e..fa08ae4df 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -144,6 +144,50 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do end describe "user timelines" do + test "respects blocks", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + + User.block(user_one, user_two) + + {:ok, activity} = CommonAPI.post(user_two, %{"status" => "User one sux0rz"}) + {:ok, repeat, _} = CommonAPI.repeat(activity.id, user_three) + + resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == activity.id + + # Even a blocked user will deliver the full user timeline, there would be + # no point in looking at a blocked users timeline otherwise + resp = + conn + |> assign(:user, user_one) + |> get("/api/v1/accounts/#{user_two.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == activity.id + + resp = + conn + |> get("/api/v1/accounts/#{user_three.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == repeat.id + + # When viewing a third user's timeline, the blocked users will NOT be + # shown. + resp = + conn + |> assign(:user, user_one) + |> get("/api/v1/accounts/#{user_three.id}/statuses") + + assert [] = json_response(resp, 200) + end + test "gets a users statuses", %{conn: conn} do user_one = insert(:user) user_two = insert(:user) @@ -891,7 +935,7 @@ test "getting a list of mutes", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) conn = conn @@ -906,7 +950,7 @@ test "getting a list of blocks", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) conn = conn diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index fa55a7cf9..6635ea7a2 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -137,55 +137,151 @@ test "paginates notifications using min_id, since_id, max_id, and limit", %{conn assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result end - test "filters notifications using exclude_visibilities", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) + describe "exclude_visibilities" do + test "filters notifications for mentions", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) - {:ok, public_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"}) + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"}) - {:ok, direct_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) + {:ok, direct_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"}) + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"}) - {:ok, private_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) + {:ok, private_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) - conn = assign(conn, :user, user) + conn = assign(conn, :user, user) - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "unlisted", "private"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "unlisted", "private"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == direct_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == direct_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "unlisted", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "unlisted", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == private_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == private_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "private", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "private", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == unlisted_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == unlisted_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["unlisted", "private", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["unlisted", "private", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == public_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == public_activity.id + end + + test "filters notifications for Like activities", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "private"}) + + {:ok, _, _} = CommonAPI.favorite(public_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(direct_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(unlisted_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(private_activity.id, user) + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + refute direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["private"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + refute private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["public"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + refute public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + end + + test "filters notifications for Announce activities", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, _, _} = CommonAPI.repeat(public_activity.id, user) + {:ok, _, _} = CommonAPI.repeat(unlisted_activity.id, user) + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + end end test "filters notifications using exclude_types", %{conn: conn} do @@ -289,7 +385,7 @@ test "doesn't see notifications after muting user with notifications", %{conn: c assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2) + {:ok, _user_relationships} = User.mute(user, user2) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -310,7 +406,7 @@ test "see notifications after muting user without notifications", %{conn: conn} assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2, false) + {:ok, _user_relationships} = User.mute(user, user2, false) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -333,7 +429,7 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2) + {:ok, _user_relationships} = User.mute(user, user2) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) @@ -341,6 +437,32 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 end + test "see move notifications with `with_move` parameter", %{ + conn: conn + } do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + follower = insert(:user) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + conn = + conn + |> assign(:user, follower) + |> get("/api/v1/notifications") + + assert json_response(conn, 200) == [] + + conn = + build_conn() + |> assign(:user, follower) + |> get("/api/v1/notifications", %{"with_move" => "true"}) + + assert length(json_response(conn, 200)) == 1 + end + defp get_notification_id_by_activity(%{id: id}) do Notification |> Repo.get_by(activity_id: id) diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 7953fad62..34deeba47 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -165,15 +165,20 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end - test "search fetches remote statuses", %{conn: conn} do + test "search fetches remote statuses and prefers them over other results", %{conn: conn} do capture_log(fn -> + {:ok, %{id: activity_id}} = + CommonAPI.post(insert(:user), %{ + "status" => "check out https://shitposter.club/notice/2827873" + }) + conn = conn |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) assert results = json_response(conn, 200) - [status] = results["statuses"] + [status, %{"id" => ^activity_id}] = results["statuses"] assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index a96fd860b..5fbe947ba 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1108,7 +1108,7 @@ test "does not return users who have favorited the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) @@ -1205,7 +1205,7 @@ test "does not return users who have reblogged the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 61b6cea75..dc17cc963 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -194,7 +194,7 @@ test "doesn't include DMs from blocked users", %{conn: conn} do blocker = insert(:user) blocked = insert(:user) user = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) {:ok, _blocked_direct} = CommonAPI.post(blocked, %{ diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index d147079ab..2107bb85c 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -66,6 +66,7 @@ test "Represent a user account" do note: "valid html", sensitive: false, pleroma: %{ + actor_type: "Person", discoverable: false }, fields: [] @@ -92,17 +93,11 @@ test "Represent a user account" do test "Represent the user account for the account owner" do user = insert(:user) - notification_settings = %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } - + notification_settings = %Pleroma.User.NotificationSetting{} privacy = user.default_scope assert %{ - pleroma: %{notification_settings: ^notification_settings}, + pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, source: %{privacy: ^privacy} } = AccountView.render("show.json", %{user: user, for: user}) end @@ -112,7 +107,8 @@ test "Represent a Service(bot) account" do insert(:user, %{ follower_count: 3, note_count: 5, - source_data: %{"type" => "Service"}, + source_data: %{}, + actor_type: "Service", nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -140,6 +136,7 @@ test "Represent a Service(bot) account" do note: user.bio, sensitive: false, pleroma: %{ + actor_type: "Service", discoverable: false }, fields: [] @@ -190,9 +187,9 @@ test "represent a relationship for the following and followed user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.mute(user, other_user, true) - {:ok, user} = CommonAPI.hide_reblogs(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user, true) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) expected = %{ id: to_string(other_user.id), @@ -218,9 +215,9 @@ test "represent a relationship for the blocking and blocked user" do other_user = insert(:user) {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.block(user, other_user) - {:ok, other_user} = User.block(other_user, user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(other_user, user) expected = %{ id: to_string(other_user.id), @@ -284,14 +281,15 @@ test "represent an embedded relationship" do insert(:user, %{ follower_count: 0, note_count: 5, - source_data: %{"type" => "Service"}, + source_data: %{}, + actor_type: "Service", nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) other_user = insert(:user) {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.block(other_user, user) + {:ok, _user_relationship} = User.block(other_user, user) {:ok, _} = User.follow(insert(:user), user) expected = %{ @@ -317,6 +315,7 @@ test "represent an embedded relationship" do note: user.bio, sensitive: false, pleroma: %{ + actor_type: "Service", discoverable: false }, fields: [] diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index c9043a69a..ba1721e06 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -107,4 +107,31 @@ test "Follow notification" do assert [] == NotificationView.render("index.json", %{notifications: [notification], for: followed}) end + + test "Move notification" do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + follower = insert(:user) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + old_user = refresh_record(old_user) + new_user = refresh_record(new_user) + + [notification] = Notification.for_user(follower, %{with_move: true}) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "move", + account: AccountView.render("show.json", %{user: old_user, for: follower}), + target: AccountView.render("show.json", %{user: new_user, for: follower}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + assert [expected] == + NotificationView.render("index.json", %{notifications: [notification], for: follower}) + end end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index d46ecc646..bdd87a79e 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -183,7 +183,7 @@ test "tells if the message is muted for some reason" do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity}) @@ -199,7 +199,7 @@ test "tells if the message is thread muted" do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity, for: user}) diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 9b554601d..acae7a734 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription @@ -182,4 +183,50 @@ test "renders title for create activity with direct visibility" do assert Impl.format_title(%{activity: activity}) == "New Direct Message" end + + describe "build_content/3" do + test "returns info content for direct message with enabled privacy option" do + user = insert(:user, nickname: "Bob") + user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) + + {:ok, activity} = + CommonAPI.post(user, %{ + "visibility" => "direct", + "status" => " "direct", + "status" => + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Direct Message" + } + end + end end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 80a7541b2..8911c46b1 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.StreamerTest do alias Pleroma.Web.Streamer.StreamerSocket alias Pleroma.Web.Streamer.Worker - @moduletag needs_streamer: true + @moduletag needs_streamer: true, capture_log: true clear_config_all([:instance, :skip_thread_containment]) describe "user streams" do @@ -59,7 +59,7 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl user: user } do blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) @@ -259,7 +259,7 @@ test "it sends message if recipients invalid and thread containment is enabled b test "it doesn't send messages involving blocked users" do user = insert(:user) blocked_user = insert(:user) - {:ok, user} = User.block(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) task = Task.async(fn -> @@ -301,7 +301,7 @@ test "it doesn't send messages transitively involving blocked users" do "public" => [fake_socket] } - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"}) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index f0211f59c..734cd2211 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -159,11 +159,31 @@ test "it updates notification settings", %{conn: conn} do user = Repo.get(User, user.id) - assert %{ - "followers" => false, - "follows" => true, - "non_follows" => true, - "non_followers" => true + assert %Pleroma.User.NotificationSetting{ + followers: false, + follows: true, + non_follows: true, + non_followers: true, + privacy_option: false + } == user.notification_settings + end + + test "it update notificatin privacy option", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"}) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + followers: true, + follows: true, + non_follows: true, + non_followers: true, + privacy_option: true } == user.notification_settings end end @@ -387,7 +407,7 @@ test "returns error when user is blocked", %{conn: conn} do user = insert(:user) user2 = insert(:user) - {:ok, _user} = Pleroma.User.block(user2, user) + {:ok, _user_block} = Pleroma.User.block(user2, user) response = conn @@ -485,7 +505,7 @@ test "returns error when user is blocked", %{conn: conn} do Pleroma.Config.put([:user, :deny_follow_blocked], true) user = insert(:user) user2 = insert(:user) - {:ok, _user} = Pleroma.User.block(user2, user) + {:ok, _user_block} = Pleroma.User.block(user2, user) response = conn